Home/Learn/React/Cache Behaviour — Staleness, Refetching and Invalidation

Cache Behaviour — Staleness, Refetching and Invalidation

Advanced
Data

Every 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.

Hierarchical keys and a key factory
// 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.

Triggers, and choosing staleTime by data
// 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 cache

Two Failure Modes

The bugs that actually happen, and the fix for each.

Coarse keys and missing invalidation
// 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 Aria
1

How would you structure query keys so invalidation can be broad or narrow?

Hard
2

What causes a list to show the previous filter's data?

Medium
3

When would you disable refetch-on-window-focus?

Medium

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.

Loading discussion…