- React
- State Architecture, Store Subscriptions, and Server Cache
- Server State vs Client State: TanStack Query Architecture
Server State vs Client State: TanStack Query Architecture
TanStack Query manages server state as an asynchronous remote cache governed by stale-while-revalidate semantics. It eliminates client-side data fetching boilerplate by automating background refetching, request deduplication, cache garbage collection, and optimistic UI updates.
Key Learning Objectives
Differentiate server state (remote, async, shared, stale) from client state (local, sync, ephemeral).
Distinguish staleTime (how long data is considered fresh) from gcTime (how long inactive cache persists in memory).
Implement query invalidation and optimistic mutations with rollback on network failure.
Explain request deduplication across concurrent component mounts sharing the same query key.
The Interview Problem & Context
Explain the difference between staleTime and gcTime in TanStack Query and implement an optimistic mutation pattern with automatic error rollback.
1import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';23interface Todo {4 id: string;5 title: string;6 completed: boolean;7}89export function useTodos() {10 const queryClient = useQueryClient();1112 // 1. Query: Stale-while-revalidate caching13 const todosQuery = useQuery<Todo[]>({14 queryKey: ['todos'],15 queryFn: async () => (await fetch('/api/todos')).json(),16 staleTime: 1000 * 60 * 5, // Fresh for 5 minutes (no background refetch during this time)17 gcTime: 1000 * 60 * 30, // Kept in memory for 30 minutes after all subscribers unmount18 });1920 // 2. Optimistic Mutation: Immediate UI feedback + rollback on error21 const addMutation = useMutation({22 mutationFn: async (newTodo: { title: string }) => {23 const res = await fetch('/api/todos', {24 method: 'POST',25 headers: { 'Content-Type': 'application/json' },26 body: JSON.stringify(newTodo),27 });28 if (!res.ok) throw new Error('Failed to create todo');29 return res.json();30 },31 // Before network request fires:32 onMutate: async (newTodo) => {33 await queryClient.cancelQueries({ queryKey: ['todos'] });34 const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);3536 // Optimistically update cache with temporary ID37 queryClient.setQueryData<Todo[]>(['todos'], (old = []) => [38 ...old,39 { id: 'temp-' + Date.now(), title: newTodo.title, completed: false },40 ]);4142 return { previousTodos }; // Context passed to onError43 },44 // If request fails: rollback to saved snapshot45 onError: (err, newTodo, context) => {46 if (context?.previousTodos) {47 queryClient.setQueryData(['todos'], context.previousTodos);48 }49 },50 // Always refetch in background after completion to sync server truth51 onSettled: () => {52 queryClient.invalidateQueries({ queryKey: ['todos'] });53 },54 });5556 return { ...todosQuery, addTodo: addMutation.mutate };57}
Deep Architectural Explanation
The Crucial Difference: staleTime vs gcTime
Understanding this distinction is the single most tested TanStack Query interview question:
| Concept | Definition | Default Value | What Happens When Expired? |
|---|---|---|---|
| staleTime | Duration data is considered fresh. | 0 ms | Data is stale. Next component mount or window focus triggers a background refetch. |
| gcTime (formerly cacheTime) | Duration inactive data is kept in memory after all consumers unmount. | 5 minutes | Data is garbage collected from memory. Next mount starts with empty cache (shows spinner). |
Stale-While-Revalidate Flow:
- Component mounts: TanStack Query immediately serves cached data from memory (instant UI).
- Because data is "stale" (exceeded
staleTime), it triggers a background network request. - Once the server responds, it seamlessly updates the cache and component re-renders with fresh data.
Optimistic UI Mechanics:
onMutate: Save snapshot of previous cache, write optimistic draft item to cache.onError: If network fails, restore snapshot fromcontext.previousTodos.onSettled: Invalidate query to reconcile server-assigned IDs and timestamps.
Common Interview Traps & Pitfalls
Confusing staleTime with gcTime and setting staleTime without realizing gcTime purges cache after unmounting.
Using useEffect to manually trigger API refetches on component mount instead of relying on query keys.
Forgetting to cancel in-flight queries in onMutate, causing race condition overwrites.
Senior Interviewer Follow-Up Probes
How does query key serialization work for complex objects like `["todos", { status, page }]`?
What are the advantages of prefetching queries during router navigation or link hovers?
How does TanStack Query handle infinite pagination using `useInfiniteQuery` and `getNextPageParam`?
Frequently Asked Interview Questions
What is the difference between staleTime and cacheTime (gcTime)?
staleTime controls freshness and refetch triggers. gcTime controls garbage collection duration for inactive cache data in memory after all observing components unmount.
How does TanStack Query deduplicate identical network requests?
If three components render concurrently and call useQuery with the exact same query key, TanStack Query executes only one network request and broadcasts the resolved promise result to all three components.
Why should mutations invalidate queries rather than manually updating cache?
Invalidating queries forces a refetch that guarantees synchronization with server-side business logic, database triggers, timestamps, and permissions, preventing client/server state divergence.
What is structural sharing in TanStack Query?
When a background refetch completes, TanStack Query compares the new JSON data with the existing cached JSON. It preserves referential equality for identical nested sub-objects, preventing unnecessary component re-renders.
Can TanStack Query be used with WebSockets or SSE?
Yes. Real-time events from WebSockets can directly call queryClient.setQueryData to update active query caches instantaneously without waiting for a REST poll.
