Cache Invalidation
After a mutation, mark cached data as stale (or replace it) so the UI shows up-to-date data without unnecessary refetches.
The problem I keep seeing
You update a resource (e.g. edit profile, add a post). The server succeeds, but the client still has the old data in its cache. If you don’t invalidate (or update) that cache, the user sees stale content until the next refetch or page load. You need a consistent way to say “this data is out of date” after a mutation so that the next read gets fresh data—or you update the cache directly when the server returns the new resource.
The flip side: invalidating too broadly causes a storm of refetches; invalidating too narrowly leaves other views stale. You want to invalidate the right keys (and optionally use setQueryData when you already have the new data).
Naive approach
Run the mutation and do nothing to the cache. Rely on staleTime or the user navigating away and back to trigger a refetch. The UI can stay stale until then.
First improvement
In onSuccess (or onSettled), call queryClient.invalidateQueries({ queryKey: ['user', userId] }). Every active query with that key is marked stale and refetched. Views that display that user’s data update automatically.
Why this helps: One place declares “user X is stale”; all consumers refetch. No manual prop drilling or callback chains to refresh data.
Remaining issues
- Key design: Use a consistent query key hierarchy (e.g.
['user', id],['feed']) so you can invalidate by exact key or by prefix when a mutation affects multiple views. - setQueryData vs invalidate: If the mutation response returns the full updated resource, you can
setQueryDataand skip a refetch. If other derived data (e.g. a list that includes this item) is affected, invalidate those keys too. - refetchType:
invalidateQuerieswithrefetchType: 'active'only refetches queries that currently have observers (mounted components). UserefetchType: 'all'sparingly (e.g. after logout) to clear everything.
Production pattern
After every mutation that changes server state, either invalidate the affected query keys or set the cache from the response. Prefer invalidation when multiple consumers need fresh data or when the response doesn’t include the full resource. Use setQueryData when the API returns the new object and you want to avoid an extra request.
When I use this
- Invalidate: After create/update/delete that affects a list or detail view. Invalidate the list key and the detail key for the changed resource.
- setQueryData: When the mutation response is the full new entity and you don’t need to refetch related data. Combine with invalidation for keys that aggregate this entity (e.g. feed).
- Don’t over-invalidate: Avoid invalidating the whole cache (e.g.
queryKey: []or no predicate) on every mutation; it causes a thundering herd of refetches.
Optimistic + invalidation
onMutate and invalidate in onSettled so the server remains the source of truth. Invalidation then refetches and corrects any drift.Gotchas
- Key matching:
invalidateQueries({ queryKey: ['user'] })matches all keys that start with['user'](e.g.['user', '1']). Useexact: trueto match only['user']. - Timing: Invalidate in
onSettled(not onlyonSuccess) so you refetch after an error too—e.g. if the mutation failed after an optimistic update, the refetch restores the previous data. - Dependent queries: If view B depends on data from query A (e.g. a detail page that uses the list’s item), invalidate A when the detail is updated so the list reflects the change when the user goes back.
Data Fetching & Sync → · Optimistic Updates → · All patterns