React Curriculum

Server State vs Client State: TanStack Query Architecture

Medium100% Free~30 mins#react#tanstack-query#react-query#server-state#caching#optimistic-ui
Interview Answer (30-Second Summary)

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';
2
3interface Todo {
4 id: string;
5 title: string;
6 completed: boolean;
7}
8
9export function useTodos() {
10 const queryClient = useQueryClient();
11
12 // 1. Query: Stale-while-revalidate caching
13 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 unmount
18 });
19
20 // 2. Optimistic Mutation: Immediate UI feedback + rollback on error
21 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']);
35
36 // Optimistically update cache with temporary ID
37 queryClient.setQueryData<Todo[]>(['todos'], (old = []) => [
38 ...old,
39 { id: 'temp-' + Date.now(), title: newTodo.title, completed: false },
40 ]);
41
42 return { previousTodos }; // Context passed to onError
43 },
44 // If request fails: rollback to saved snapshot
45 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 truth
51 onSettled: () => {
52 queryClient.invalidateQueries({ queryKey: ['todos'] });
53 },
54 });
55
56 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:

ConceptDefinitionDefault ValueWhat Happens When Expired?
staleTimeDuration data is considered fresh.0 msData 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 minutesData is garbage collected from memory. Next mount starts with empty cache (shows spinner).

Stale-While-Revalidate Flow:

  1. Component mounts: TanStack Query immediately serves cached data from memory (instant UI).
  2. Because data is "stale" (exceeded staleTime), it triggers a background network request.
  3. Once the server responds, it seamlessly updates the cache and component re-renders with fresh data.

Optimistic UI Mechanics:

  1. onMutate: Save snapshot of previous cache, write optimistic draft item to cache.
  2. onError: If network fails, restore snapshot from context.previousTodos.
  3. 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 default value of staleTime in TanStack Query?

staleTime defaults to 0 milliseconds. This means query results are immediately considered stale upon retrieval, triggering background refetches on every mount or window focus.

staleTime controls freshness and refetch triggers. gcTime controls garbage collection duration for inactive cache data in memory after all observing components unmount.

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.

Invalidating queries forces a refetch that guarantees synchronization with server-side business logic, database triggers, timestamps, and permissions, preventing client/server state divergence.

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.

Yes. Real-time events from WebSockets can directly call queryClient.setQueryData to update active query caches instantaneously without waiting for a REST poll.