Execution contract
- Use when
- Use when a mutation returns the authoritative current entity and can update an exact cache entry.
- Avoid when
- Prefer bounded invalidation when the response is partial or affects an unknown set of lists.
- Preconditions
- List affected query keys, the server response type, and the old value that must survive failure.
- Verification
- After success, detail and list agree; after failure, old cache remains and unrelated queries are not invalidated.
- Failure mode
- Divergent list and detail data indicates one cache was skipped or keys differ; centralize key factories and update scope.
- Security
- A client cache write is not proof of persistence or authorization; use server-confirmed responses only.
Write through directly
mutationFn returns the server-created object. onSuccess creates a new array with functional setQueryData. Never mutate current in place.
export function useCreateIssue() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: createIssue,
onSuccess: (created) => {
queryClient.setQueryData<Issue[]>(['issues'], (current = []) => [
created,
...current,
])
},
})
}When to invalidate
If the server reorders lists, computes aggregates, applies permission filters, or omits relevant fields, await invalidateQueries({ queryKey: ['issues'] }) after success. Do not blindly set and invalidate together.
Errors and concurrency
Use isPending to prevent duplicate submission or show progress and isError for recoverable feedback. Add optimistic updates only after implementing cancellation, snapshot, and rollback.