Understanding caching is crucial for performant data fetching.
Why Cache?
- Performance: Instant display of cached data
- Reduce Server Load: Fewer API calls
- Offline Support: Show data without network
- Better UX: No loading spinners for repeat visits
Cache Freshness
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ FRESH │ -> │ STALE │ -> │ GARBAGE │
│ (staleTime) │ │ (gcTime) │ │ (deleted) │
└─────────────┘ └─────────────┘ └─────────────┘
Fresh Data
- Recently fetched
- Won't trigger refetch
- Displayed immediately
Stale Data
- Past staleTime
- Displayed immediately BUT...
- Triggers background refetch
Garbage Collection
- No active subscribers
- Past gcTime
- Removed from cache
Stale-While-Revalidate
The SWR pattern:
- Return cached (stale) data immediately
- Revalidate in background
- Update UI when fresh data arrives
jsx// User sees data instantly, gets fresh data shortly after useQuery({ queryKey: ['todos'], queryFn: fetchTodos, staleTime: 0, // Consider stale immediately });
Cache Time vs Stale Time
jsxuseQuery({ queryKey: ['todos'], queryFn: fetchTodos, staleTime: 5 * 60 * 1000, // Fresh for 5 minutes gcTime: 30 * 60 * 1000, // Keep in cache for 30 minutes });
Scenario:
- User fetches todos at 10:00
- User navigates away at 10:02
- User returns at 10:04 (within staleTime)
- Data displayed, NO refetch
- User returns at 10:10 (past staleTime, within gcTime)
- Data displayed, background refetch
- User returns at 10:45 (past gcTime)
- Loading spinner, fresh fetch
Prefetching
jsxconst queryClient = useQueryClient(); // Prefetch on hover function TodoItem({ todo }) { const prefetchDetails = () => { queryClient.prefetchQuery({ queryKey: ['todo', todo.id], queryFn: () => fetchTodoDetails(todo.id), }); }; return ( <Link to={`/todo/${todo.id}`} onMouseEnter={prefetchDetails} > {todo.title} </Link> ); }