Skip to content
GeeksSmith
advanced 8 min read

TanStack Query & Server State Management

TanStack Query (React Query) is a dedicated server-state management library providing declarative data fetching, automatic caching, deduplication, background revalidation (SWR), and garbage collection.

Why it matters in interviews

In Staff/Lead interviews, treating server data as global client state (e.g., storing API responses in Redux) is a major red flag. Interviewers expect candidates to understand cache keys, staleTime vs gcTime, structural sharing, and mutation rollbacks.

Visual & Interactive Explanation

Interactive Flow Execution

Code Examples & Implementation

Query Key Hierarchy & Surgical Invalidation
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

// Standard query key factory pattern
export const todoKeys = {
  all: ["todos"] as const,
  lists: () => [...todoKeys.all, "list"] as const,
  list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
  details: () => [...todoKeys.all, "detail"] as const,
  detail: (id: string) => [...todoKeys.details(), id] as const,
};

export function useTodoList(filter: string) {
  return useQuery({
    queryKey: todoKeys.list(filter),
    queryFn: () => fetch(`/api/todos?filter=${filter}`).then((r) => r.json()),
    staleTime: 1000 * 60 * 2, // 2 minutes fresh
    gcTime: 1000 * 60 * 10,   // 10 minutes retention
  });
}

export function useAddTodo() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (newTodo: { title: string }) =>
      fetch("/api/todos", { method: "POST", body: JSON.stringify(newTodo) }),
    onSuccess: () => {
      // Invalidate all todo lists without invalidating unrelated keys
      queryClient.invalidateQueries({ queryKey: todoKeys.lists() });
    },
  });
}
Optimistic Mutation with Snapshot Rollback
export function useToggleTodo() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, completed }: { id: string; completed: boolean }) =>
      fetch(`/api/todos/${id}`, {
        method: "PATCH",
        body: JSON.stringify({ completed }),
      }),
    onMutate: async ({ id, completed }) => {
      // Cancel outgoing refetches to avoid overwriting optimistic update
      await queryClient.cancelQueries({ queryKey: ["todos"] });

      // Snapshot the previous value
      const previousTodos = queryClient.getQueryData<Todo[]>(["todos"]);

      // Optimistically update the cache
      queryClient.setQueryData<Todo[]>(["todos"], (old) =>
        old?.map((t) => (t.id === id ? { ...t, completed } : t))
      );

      // Return context with snapshot
      return { previousTodos };
    },
    onError: (_err, _variables, context) => {
      // Rollback to snapshot on network failure
      if (context?.previousTodos) {
        queryClient.setQueryData(["todos"], context.previousTodos);
      }
    },
    onSettled: () => {
      // Always re-sync with server truth
      queryClient.invalidateQueries({ queryKey: ["todos"] });
    },
  });
}

Interview-Ready Answers

TanStack Query manages asynchronous server state through query caching, automatic deduplication, background refetching (SWR), and granular cache invalidation via query keys.

Follow-up Questions & Deep Dives

🎯 Code Output — What Does This Print?

Question 1hard
// Component A and Component B mount simultaneously on screen:
function ComponentA() {
  const { data } = useQuery({
    queryKey: ["user", "123"],
    queryFn: () => fetchUser("123"), // takes 200ms
  });
  return <div>{data?.name}</div>;
}

function ComponentB() {
  const { data } = useQuery({
    queryKey: ["user", "123"],
    queryFn: () => fetchUser("123"),
  });
  return <div>{data?.role}</div>;
}
// How many actual network calls are dispatched?
Question 2medium
// Query has staleTime = 10_000 (10s) and gcTime = 300_000 (5m)
// Component unmounts at t = 5s.
// User navigates back to the page at t = 8s.
// Does useQuery trigger a network fetch?

Common Mistakes & Anti-Patterns

  • Leaving staleTime at 0 (default), resulting in refetches on every single component mount and window focus
  • Putting non-serializable objects or functions inside queryKey arrays
  • Using useState + useEffect for data fetching instead of a dedicated cache layer
  • Forgetting to return the snapshot context from onMutate during optimistic updates

Real-World Architectural Scenario

In an enterprise order management portal, when users switch between browser tabs, the screen flashes loading spinners on 15 different widgets. How do you fix this?

Rate Your Readiness

Rate how comfortably you can explain this in an interview to update your global readiness gauge:

Rate your confidence: