When I was at Thinking Machines, I got a really exciting opportunity to tech lead a web app project for a large petrochemical company (first time tech leading!). But this wasn't just any ordinary web app. A primary requirement of the app was to go up to 10 hours of offline usage, where mutations to data are stored temporarily in the web app and are synced to the cloud once the device is reconnected to the internet.
As this is a really unique use case, admittedly, neither me nor a lot of engineers at TM had experience building offline web apps. Since we're already planning to build this as a Progressive Web App, my team and I gravitated to service workers. Service workers can intercept requests even when your application is offline, so this was naturally a huge part in our architecture. Until I discovered that there was an easier way of doing this: enter React Query's Persist Query Client.
How it works
React Query's persistQueryClient
is a wrapper around the default queryClient
where you can utilize persisters (i.e. local storage or Indexed DB) to cache data on a specified duration.
One of the first things we needed to do is to pass a gcTime
value to queryClient
to override its default 5-minute garbage collection duration. From the official documentation:
It should be set as the same value or higher than persistQueryClient's maxAge option. E.g. if maxAge is 24 hours (the default) then gcTime should be 24 hours or higher. If lower than maxAge, garbage collection will kick in and discard the stored cache earlier than expected.
Afterwards, we created an Indexed DB persister. Indexed DB, compared to Web Storage, is faster, can store more than 5MB, and doesn't require data serialization.
import { get, set, del } from "idb-keyval";
import {
PersistedClient,
Persister,
} from "@tanstack/react-query-persist-client";
/**
* Creates an Indexed DB persister
* @see https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
*/
export function createIDBPersister(idbValidKey: IDBValidKey = "reactQuery") {
return {
persistClient: async (client: PersistedClient) => {
await set(idbValidKey, client);
},
restoreClient: async () => {
return await get<PersistedClient>(idbValidKey);
},
removeClient: async () => {
await del(idbValidKey);
},
} as Persister;
}
We then instantiated a <PersistQueryClientProvider />
component that uses the IDB persister. Once certain mutations fire offline, React Query will automatically cache them based on mutation config.
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister: persisterdb }}
onSuccess={() => {
queryClient.resumePausedMutations().then(() => {
queryClient.invalidateQueries();
});
}}
>
{children}
</PersistQueryClientProvider>
queryClient.resumePausedMutations()
here is a utility where React Query "resumes" mutations that have been made without internet connection, which was perfect for our use case.
One last piece of the puzzle was specifying the defaults for the mutations that should be cached when they're fired offline. A convenient way of doing this was to call queryClient.setMutationDefaults()
.
queryClient.setMutationDefaults([ADD_TODO], {
mutationFn: addTodo,
onSuccess: (data) => {
// do something here after addTodo() succeeds
},
});
It's important to specify mutationFn
here because when the page is reloaded, React Query wouldn't know how to resume the cached mutations unless a default mutationFn
is passed. As the docs explain:
If you persist offline mutations with the persistQueryClient plugin, mutations cannot be resumed when the page is reloaded unless you provide a default mutation function. This is a technical limitation. When persisting to an external storage, only the state of mutations is persisted, as functions cannot be serialized. After hydration, the component that triggers the mutation might not be mounted, so calling resumePausedMutations might yield an error: No mutationFn found.
After all of these have been set up, mutations made in our web app were cached when offline and are fired automatically once back online! No more writing service worker scripts from scratch.