diff --git a/docs/src/pages/guides/mutations.md b/docs/src/pages/guides/mutations.md index 47e32fafe6..ac38a5a424 100644 --- a/docs/src/pages/guides/mutations.md +++ b/docs/src/pages/guides/mutations.md @@ -195,7 +195,7 @@ useMutation(addTodo, { mutate(todo, { onSuccess: (data, error, variables, context) => { // Will execute only once, for the last mutation (Todo 3), - // regardless which mutation resolves first + // regardless which mutation resolves first }, }) }) @@ -279,6 +279,49 @@ hydrate(queryClient, state) queryClient.resumePausedMutations() ``` +### Persisting Offline mutations + +If you persist offline mutations with the [persistQueryClient plugin](../plugins/persistQueryClient), 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 triggeres the mutation might not be mounted, so calling `resumePausedMutations` might yield an error: `No mutationFn found`. + +```js +const persister = createWebStoragePersister({ + storage: window.localStorage, +}) +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + cacheTime: 1000 * 60 * 60 * 24, // 24 hours + }, + }, +}) + +// we need a default mutation function so that paused mutations can resume after a page reload +queryClient.setMutationDefaults(['todos'], { + mutationFn: ({ id, data }) => { + return api.upateTodo(id, data) + }, +}) + +export default function App() { + return ( + { + // resume mutations after initial restore from localStorage was successful + queryClient.resumePausedMutations() + }} + > + + + ) +} +``` + +We also have an extensive [offline example](../examples/offline) that covers both queries and mutations. + ## Further reading For more information about mutations, have a look at [#12: Mastering Mutations in React Query](../community/tkdodos-blog#12-mastering-mutations-in-react-query) from diff --git a/src/persistQueryClient/persist.ts b/src/persistQueryClient/persist.ts index 52ecb7126e..13af8d6253 100644 --- a/src/persistQueryClient/persist.ts +++ b/src/persistQueryClient/persist.ts @@ -119,15 +119,28 @@ export async function persistQueryClientSave({ } /** - * Subscribe to QueryCache updates (for persisting) + * Subscribe to QueryCache and MutationCache updates (for persisting) * @returns an unsubscribe function (to discontinue monitoring) */ export function persistQueryClientSubscribe( props: PersistedQueryClientSaveOptions ) { - return props.queryClient.getQueryCache().subscribe(() => { - persistQueryClientSave(props) - }) + const unsubscribeQueryCache = props.queryClient + .getQueryCache() + .subscribe(() => { + persistQueryClientSave(props) + }) + + const unusbscribeMutationCache = props.queryClient + .getMutationCache() + .subscribe(() => { + persistQueryClientSave(props) + }) + + return () => { + unsubscribeQueryCache() + unusbscribeMutationCache() + } } /** diff --git a/src/persistQueryClient/tests/persist.test.tsx b/src/persistQueryClient/tests/persist.test.tsx new file mode 100644 index 0000000000..da583a38fc --- /dev/null +++ b/src/persistQueryClient/tests/persist.test.tsx @@ -0,0 +1,49 @@ +import { createQueryClient } from '../../reactjs/tests/utils' +import { sleep } from '../../core/utils' +import { + PersistedClient, + Persister, + persistQueryClientSubscribe, +} from '../persist' + +const createMockPersister = (): Persister => { + let storedState: PersistedClient | undefined + + return { + async persistClient(persistClient: PersistedClient) { + storedState = persistClient + }, + async restoreClient() { + await sleep(10) + return storedState + }, + removeClient() { + storedState = undefined + }, + } +} + +describe('persistQueryClientSubscribe', () => { + test('should persist mutations', async () => { + const queryClient = createQueryClient() + + const persister = createMockPersister() + + const unsubscribe = persistQueryClientSubscribe({ + queryClient, + persister, + dehydrateOptions: { shouldDehydrateMutation: () => true }, + }) + + queryClient.getMutationCache().build(queryClient, { + mutationFn: async (text: string) => text, + variables: 'todo', + }) + + const result = await persister.restoreClient() + + expect(result?.clientState.mutations).toHaveLength(1) + + unsubscribe() + }) +})