SoulCache

React Adapter

Learn how to use SoulCache with React

React Adapter

The React adapter provides hooks and components for seamless React integration.

Installation

npm install @soulcache/react

Provider

Wrap your app with the provider:

import { SoulCacheProvider } from '@soulcache/react';
import { QueryClient } from '@soulcache/core';
 
const client = new QueryClient();
 
function App() {
  return (
    <SoulCacheProvider client={client}>
      <MyApp />
    </SoulCacheProvider>
  );
}

Hooks

useQuery

import { useQuery } from '@soulcache/react';
 
function Users() {
  const { data, status, error, isLoading, isFetching, isError, isSuccess } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json()),
  });
 
  if (isLoading) return <Spinner />;
  if (isError) return <Error message={error?.message} />;
 
  return (
    <ul>
      {data?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Options:

OptionTypeDefaultDescription
queryKeyQueryKeyCache identification key (required)
queryFn() => Promise<T>Fetch function (required)
enabledbooleantrueWhether the query auto-fetches
suspensebooleanfalseThrows a promise for React Suspense
throwOnErrorbooleanfalseThrows the error for error boundaries
onSuccess(data: T) => voidCallback on successful fetch
onError(error: Error) => voidCallback on fetch error

Returns:

PropertyTypeDescription
dataT | undefinedCurrent query data
errorError | nullCurrent error
statusQueryStatus'idle' | 'loading' | 'success' | 'error' | 'fetching'
fetchStatusFetchStatus'idle' | 'fetching' | 'paused'
isLoadingbooleanLoading with no data yet
isFetchingbooleanAny fetch in progress
isErrorbooleanError state
isSuccessbooleanSuccess state
isIdlebooleanIdle state
dataUpdatedAtnumberTimestamp of last update

useMutation

import { useMutation, useQueryClient } from '@soulcache/react';
 
function CreateUser() {
  const queryClient = useQueryClient();
  const mutation = useMutation({
    mutationFn: (newUser) =>
      fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(newUser),
      }).then(r => r.json()),
    onSuccess: () => {
      queryClient.invalidateQueries(['users']);
    },
  });
 
  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      mutation.mutate({ name: 'John' });
    }}>
      <button disabled={mutation.isPending}>
        {mutation.isPending ? 'Creating...' : 'Create User'}
      </button>
    </form>
  );
}

Options:

OptionTypeDescription
mutationFn(variables: TVariables) => Promise<TData>The mutation function (required)
onMutate(variables: TVariables) => unknownPre-mutation callback (return context)
onSuccess(data, variables, context) => voidPost-success callback
onError(error, variables, context) => voidPost-error callback
onSettled(data, error, variables, context) => voidPost-settle callback

Returns:

PropertyTypeDescription
dataTData | undefinedMutation result data
errorError | nullMutation error
statusMutationStatus'idle' | 'pending' | 'success' | 'error'
isPendingbooleanCurrently executing
isSuccessbooleanSucceeded
isErrorbooleanErrored
isIdlebooleanNot yet triggered
mutate(variables: TVariables) => voidTrigger mutation
mutateAsync(variables: TVariables) => Promise<TData>Trigger mutation (returns promise)
reset() => voidReset mutation state

useInfiniteQuery

import { useInfiniteQuery } from '@soulcache/react';
 
function PostList() {
  const {
    data, fetchNextPage, fetchPreviousPage,
    hasNextPage, hasPreviousPage,
    isFetchingNextPage, isFetchingPreviousPage,
    pageCount,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam }) =>
      fetch(`/api/posts?page=${pageParam}`).then(r => r.json()),
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    getPreviousPageParam: (firstPage) => firstPage.prevCursor,
    initialPageParam: 0,
  });
 
  return (
    <div>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.data.map(post => (
            <Post key={post.id} post={post} />
          ))}
        </div>
      ))}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading...' : 'Load More'}
      </button>
    </div>
  );
}

useQueryClient

Access the QueryClient instance for imperative operations:

import { useQueryClient } from '@soulcache/react';
 
function SearchButton() {
  const queryClient = useQueryClient();
 
  return (
    <button onClick={() => queryClient.fetchQuery({
      queryKey: ['search', query],
      queryFn: () => search(query),
    })}>
      Search
    </button>
  );
}

usePrefetchQuery

Returns a callback that silently prefetches data:

import { usePrefetchQuery } from '@soulcache/react';
 
function UserLink({ userId }) {
  const prefetch = usePrefetchQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });
 
  return (
    <Link onMouseEnter={prefetch} to={`/users/${userId}`}>
      View User
    </Link>
  );
}

useIsFetching

Returns the count of currently fetching queries:

import { useIsFetching } from '@soulcache/react';
 
function GlobalSpinner() {
  const isFetching = useIsFetching();
  return isFetching > 0 ? <Spinner /> : null;
}

useIsMutating

Returns the count of currently pending mutations:

import { useIsMutating } from '@soulcache/react';
 
function MutationStatus() {
  const isMutating = useIsMutating();
  return isMutating > 0 ? <SavingIndicator /> : null;
}

DevTools

Enable devtools for debugging:

import { SoulCacheDevToolsPanel } from '@soulcache/devtools';
 
function App() {
  return (
    <SoulCacheProvider client={client}>
      <MyApp />
      <SoulCacheDevToolsPanel />
    </SoulCacheProvider>
  );
}

Best Practices

React Guidelines

  • Wrap once — Provider should be at the root of your app
  • Stable keys — Don't create new arrays in render
  • Error boundaries — Always wrap with error boundaries
  • Cleanup — Provider handles cleanup automatically

On this page