← Back to the library
FrontendCore · 7 min

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

  1. 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.
  2. With TanStack Query, invalidate by key prefix on success and await it so isPending covers the refetch.
  3. For optimistic updates, cancel in-flight queries, snapshot, roll back on error, and invalidate on settle.
  4. 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.
  5. Inspect Cache-Control on API responses: per-user JSON should not be cacheable by shared caches; no-cache still stores but revalidates.
  6. 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?

SOURCES

Guide reviewed