renderpx
Theme: auto

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.

tsx
Loading...

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.

tsx
Loading...

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 setQueryData and skip a refetch. If other derived data (e.g. a list that includes this item) is affected, invalidate those keys too.
  • refetchType: invalidateQueries with refetchType: 'active' only refetches queries that currently have observers (mounted components). Use refetchType: '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.

tsx
Loading...
Optional: setQueryData when response has full resourcetsx
Loading...

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

With optimistic updates, you apply the change in 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']). Use exact: true to match only ['user'].
  • Timing: Invalidate in onSettled (not only onSuccess) 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