The UI shows old data after a save
“The save succeeds, but the list, detail page, or another tab keeps showing the previous value until a hard refresh.”
THE PRINCIPLE
A mutation is only finished when every view that depends on the data has been invalidated or updated. Design query keys and cache tags around entities, not screens.
FIRST MOVES
- Check the network panel after the save: no request means a client cache answered; a 200 with old data means a server or CDN cache did.
- With TanStack Query, invalidate by key prefix on success and await it so isPending covers the refetch.
- For optimistic updates, cancel in-flight queries, snapshot, roll back on error, and invalidate on settle.
- In the Next.js App Router, call updateTag or revalidatePath in the Server Action that performs the write; revalidateTag with a profile serves stale content while refreshing.
- Inspect Cache-Control on API responses: per-user JSON should not be cacheable by shared caches; no-cache still stores but revalidates.
- Confirm the read does not hit a lagging replica right after the write.
TOOLS: THEN → NOW
Global stores refetched by hand after each writeTanStack Query, SWR, or RTK Query with key-based invalidation
Time-based ISR for user editsTag-based invalidation (updateTag, revalidateTag) from the write path
PATTERN SNAPSHOT
const queryClient = useQueryClient();
const save = useMutation({
mutationFn: updateInvoice,
onSuccess: (saved) => {
// Detail view: write the server response straight into the cache.
queryClient.setQueryData(["invoice", saved.id], saved);
},
// Lists: prefix match refetches every ["invoices", ...] query.
// Returning the promise keeps isPending true until they refresh.
onSettled: () => queryClient.invalidateQueries({ queryKey: ["invoices"] }),
});CLOSE THE AI. EXPLAIN THIS.
Why can revalidatePath('/invoices') leave a dashboard that reads the same data still stale?RELATED SYMPTOMS
SOURCES
Guide reviewed