Skip to content

Commit

Permalink
fix(persistQueryClient): subscribe to both QueryCache and MutationCache
Browse files Browse the repository at this point in the history
  • Loading branch information
TkDodo committed Apr 2, 2022
1 parent 49704c7 commit 39a213e
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 5 deletions.
45 changes: 44 additions & 1 deletion docs/src/pages/guides/mutations.md
Expand Up @@ -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
},
})
})
Expand Down Expand Up @@ -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 (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
onSuccess={() => {
// resume mutations after initial restore from localStorage was successful
queryClient.resumePausedMutations()
}}
>
<RestOfTheApp />
</PersistQueryClientProvider>
)
}
```

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
Expand Down
21 changes: 17 additions & 4 deletions src/persistQueryClient/persist.ts
Expand Up @@ -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()
}
}

/**
Expand Down
49 changes: 49 additions & 0 deletions 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()
})
})

0 comments on commit 39a213e

Please sign in to comment.