Optimistic updates via UI with variables
React Query provides two approaches to optimistic updates. The simpler variant uses the `variables` returned from `useMutation` to update the UI directly without interacting with the cache. After calling `mutate`, you can access `variables` from the mutation result and render a temporary item in the UI while `isPending` is true. The temporary item automatically disappears once the mutation completes or errors.
Optimistic updates via cache with onMutate
The second approach to optimistic updates uses the `onMutate` option to update the cache directly before the mutation completes. The `onMutate` handler receives the mutation variables and a context object, and should return a rollback value that will be passed to `onError` and `onSettled` handlers. This approach automatically handles updates across multiple components that share the same cached data.
onMutate handler pattern for cache updates
The `onMutate` handler should follow this pattern: (1) Cancel any outgoing refetches using `context.client.cancelQueries()` to prevent them from overwriting the optimistic update, (2) Snapshot the previous value using `context.client.getQueryData()`, (3) Optimistically update to the new value using `context.client.setQueryData()`, (4) Return the snapshotted value for rollback purposes.
Rollback on mutation failure
If a mutation fails after an optimistic cache update, the `onError` handler can use the value returned from `onMutate` to roll back the cache to its previous state by calling `context.client.setQueryData()` with the snapshotted value.
useMutationState hook for cross-component optimistic updates
When the mutation and query live in different components, use the `useMutationState` hook to access mutations from other components. Combine it with a `mutationKey` in the mutation configuration. The hook accepts a `filters` object to filter by `mutationKey` and `status`, and a `select` function to extract the desired data. Variables will be an array because multiple mutations might run simultaneously. Use `mutation.state.submittedAt` as a unique key for displaying concurrent optimistic updates.
When to use UI-based vs cache-based optimistic updates
Use UI-based optimistic updates (via `variables`) when there is only one place where the optimistic result should be shown, as it requires less code and is easier to reason about with no rollback handling needed. Use cache-based optimistic updates (via `onMutate`) when multiple places on the screen need to know about the update, as cache manipulation automatically handles updates across all components sharing the same data.
Example: Optimistic update via UI variables for todo list
```tsx
const addTodoMutation = useMutation({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
const { isPending, variables, mutate, isError } = addTodoMutation
// In render:
<ul>
{todoQuery.items.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
{isPending && <li style={{ opacity: 0.5 }}>{variables}</li>}
</ul>
{isError && (
<li style={{ color: 'red' }}>
{variables}
<button onClick={() => mutate(variables)}>Retry</button>
</li>
)}
```
This example shows how to render a temporary item with reduced opacity while the mutation is pending, and display a retry button if the mutation errors.
Example: Optimistic cache update for adding a todo
```tsx
const queryClient = useQueryClient()
useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo, context) => {
await context.client.cancelQueries({ queryKey: ['todos'] })
const previousTodos = context.client.getQueryData(['todos'])
context.client.setQueryData(['todos'], (old) => [...old, newTodo])
return { previousTodos }
},
onError: (err, newTodo, onMutateResult, context) => {
context.client.setQueryData(['todos'], onMutateResult.previousTodos)
},
onSettled: (data, error, variables, onMutateResult, context) =>
context.client.invalidateQueries({ queryKey: ['todos'] }),
})
```
This example demonstrates optimistic cache updates when adding a todo to a list, including cancellation of refetches, optimistic update, and rollback on error.
Example: Optimistic cache update for updating a single todo
```tsx
useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo, context) => {
await context.client.cancelQueries({ queryKey: ['todos', newTodo.id] })
const previousTodo = context.client.getQueryData(['todos', newTodo.id])
context.client.setQueryData(['todos', newTodo.id], newTodo)
return { previousTodo, newTodo }
},
onError: (err, newTodo, onMutateResult, context) => {
context.client.setQueryData(
['todos', onMutateResult.newTodo.id],
onMutateResult.previousTodo,
)
},
onSettled: (newTodo, error, variables, onMutateResult, context) =>
context.client.invalidateQueries({ queryKey: ['todos', newTodo.id] }),
})
```
This example shows optimistic cache updates for updating a single todo item with rollback capability on error.
Example: Cross-component optimistic updates with useMutationState
```tsx
// In mutation component
const { mutate } = useMutation({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
mutationKey: ['addTodo'],
})
// In different component
const variables = useMutationState<string>({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables,
})
```
This example demonstrates how to access mutation state from a different component using `useMutationState` with a `mutationKey`.
Variables persist after mutation error
When a mutation errors, the `variables` from the mutation result are not cleared and remain accessible. This allows you to display the failed data in the UI, show error messages, or provide retry functionality using the original variables.