React-query Cache in Local Storage with persistQueryClient

May 30, 2022

3 min read

React-query Cache in Local Storage with persistQueryClient
Watch on YouTube

We can immediately load app state by saving the react-query cache to local storage. Let's try it out!

react-query has a utility for persisting the state of your queryClient and its caches for later use.

We can import everything from react-query. For persist to work properly, we need to pass QueryClient a cache time value to override the default during hydration. It should be at least 24 hours. Let’s set it to 5 days. To exclude some queries from being persistent, we can pass a filter function to the shouldDehydrateQuery param. To make react-query work, we'll pass the clietn to the QueryClientProvider.

import { paddleQueryKey } from "membership/paddle/hooks/usePaddleSdk"
import { QueryClient, QueryKey } from "react-query"
import { createWebStoragePersistor } from "react-query/createWebStoragePersistor-experimental"
import { persistQueryClient } from "react-query/persistQueryClient-experimental"
import { MS_IN_DAY } from "utils/time"

const cacheTime = MS_IN_DAY * 5

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime,
    },
  },
})

const localStoragePersistor = createWebStoragePersistor({
  storage: window.localStorage,
})

const doNotPersistQueries: QueryKey[] = [paddleQueryKey]

persistQueryClient({
  queryClient,
  persistor: localStoragePersistor,
  maxAge: cacheTime,
  hydrateOptions: {},
  dehydrateOptions: {
    shouldDehydrateQuery: ({ queryKey }) => {
      return !doNotPersistQueries.includes(queryKey)
    },
  },
})

I load everything the user needs with one query - userStateQuery. It's a productivity app, and there is not that much data coming from the back-end. I use graphQL query string as a key for react-query to force cache invalidation on change. The query is enabled only for a logged-in user. The query function makes a post request to the GraphQL API.

const userStateQuery = `
query userState($input: UserStateInput!) {
  userState(input: $input) {
    ...
  }
}
`

const remoteStateQueryKey = userStateQuery

interface Props {
  children: ReactNode
}

export const RemoteStateProvider = ({ children }: Props) => {
  const isLoggedIn = useIsUserLoggedIn()

  const queryClient = useQueryClient()

  const dispatch = useDispatch()

  const { data = null } = useQuery(
    remoteStateQueryKey,
    async () => {
      const remoteState: RemoteStateView = await postToMainApi({
        query: userStateQuery,
        variables: {
          input: {
            timeZone: offsetedUtils.getOffset(),
          },
        },
      })

      return remoteState
    },
    {
      keepPreviousData: true,
      refetchOnMount: false,
      refetchOnReconnect: false,
      staleTime: Infinity,
      enabled: isLoggedIn,
    }
  )

  const updateState = useCallback(
    (pieceOfState: Partial<RemoteStateView>) => {
      queryClient.setQueryData<RemoteStateView>(
        remoteStateQueryKey,
        (state) => ({
          ...((state || {}) as RemoteStateView),
          ...pieceOfState,
        })
      )
    },
    [queryClient]
  )

  return (
    <RemoteStateContext.Provider value={{ state: data, updateState }}>
      {children}
    </RemoteStateContext.Provider>
  )
}

When the user signs out from the app, I clear all the cache.

Now, when we reload the app, there's no waiting, everything loads immediately!