Cache Behaviour — Staleness, Refetching and Invalidation
AdvancedEvery cached value is a bet that the server has not changed. Deciding how long to hold that bet, and what proves it wrong, is the whole of cache design.
Overview
Caching is where "it works on my machine" bugs come from in a data-heavy frontend: two components disagree, a value updates everywhere except one place, or a stale price is shown after a change. The mental model that resolves it is small. Each cache entry has a freshness window; while fresh, nothing refetches. Outside it, the entry is still shown but refetched in the background on defined triggers. Invalidation is you telling the cache that a bet has lost. Most cache bugs are one of two things: a key that is too coarse, or an invalidation that was never written.
Key Design
The key is the contract. Hierarchical keys make invalidation expressive.
// Structure keys from general to specific
['problems'] // everything problems-related
['problems', 'list', { topic, page }] // one list
['problems', 'detail', slug] // one item
// Then invalidation can be as broad or narrow as the write requires
queryClient.invalidateQueries({ queryKey: ['problems'] }) // all
queryClient.invalidateQueries({ queryKey: ['problems', 'list'] }) // lists only
queryClient.invalidateQueries({ queryKey: ['problems', 'detail', slug] })
// A key factory keeps them consistent and typo-proof
export const problemKeys = {
all: ['problems'],
lists: () => [...problemKeys.all, 'list'],
list: (filters) => [...problemKeys.lists(), filters],
details: () => [...problemKeys.all, 'detail'],
detail: (slug) => [...problemKeys.details(), slug],
}
// The bug this prevents: one file uses ['problem', slug] and another
// uses ['problems', slug], so the invalidation silently misses.When Data Refetches
The triggers, and which defaults tend to surprise people.
// A STALE query refetches when:
// - a component using it mounts
// - the window regains focus (refetchOnWindowFocus, default on)
// - the network reconnects (refetchOnReconnect, default on)
// - refetchInterval fires (off by default)
// - you invalidate it
// A FRESH query does none of these. staleTime is the only control.
// Choosing staleTime by how the data behaves:
// a published article Infinity or hours — it barely changes
// a problem list 5 minutes
// the user's own progress 30 seconds
// a live leaderboard refetchInterval: 10_000
// Window focus refetching is excellent for dashboards and confusing
// for forms — the user tabs away, comes back, and the screen shifts.
// Turn it off where the page holds an in-progress edit.
// Manual reads and writes, when you need them
queryClient.getQueryData(problemKeys.detail(slug))
queryClient.setQueryData(problemKeys.detail(slug), updated) // seed the cacheTwo Failure Modes
The bugs that actually happen, and the fix for each.
// 1. Key too coarse — different data sharing one entry
useQuery({ queryKey: ['problems'], queryFn: () => list({ topic }) })
// Switching topic shows the previous topic's rows, because the key
// did not change. Fix: put every input in the key.
// 2. Missing invalidation — a write nobody told the cache about
await deleteSubmission(id)
// the submissions list still shows it until a reload.
// Fix: invalidate every affected key in onSuccess. Write them down
// as part of the mutation, not later.
// A useful discipline: for each mutation, write a one-line comment
// listing what it invalidates. Reviewers can then check it.
// Seeding detail from a list avoids a spinner on navigation:
onSuccess: (rows) => {
rows.forEach(r => queryClient.setQueryData(problemKeys.detail(r.slug), r))
}
// The detail page then renders instantly and refetches in the
// background for the fields the list did not carry.Key Points to Remember
- 1Hierarchical keys plus a key factory make invalidation precise and prevent typo mismatches
- 2A fresh query never refetches; staleness plus a trigger (mount, focus, reconnect, invalidate) causes refetching
- 3Choose staleTime from how fast the data actually changes, not from a habit
- 4Refetch-on-focus is good for dashboards and disruptive on pages holding an in-progress edit
- 5Almost every cache bug is a key that is too coarse or an invalidation that was never written
Interview Questions
Sign in to ask AriaHow would you structure query keys so invalidation can be broad or narrow?
What causes a list to show the previous filter's data?
When would you disable refetch-on-window-focus?
Ask Aria about Cache Behaviour — Staleness, Refetching and Invalidation
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.