TanStack Query owns asynchronous server state; it is not a replacement for every kind of React state.
Course navigationPhase 22⌄
TanStack Query
Use TanStack Query as the owner of asynchronous server state with deliberate query keys, freshness, invalidation, mutations, and pagination behavior.
Phase outcome and project increment
Learning outcome
Use TanStack Query as the owner of asynchronous server state with deliberate query keys, freshness, invalidation, mutations, and pagination behavior.
Concrete deliverable
TaskFlow query/mutation layer with stable keys, list/detail caching, invalidation, filters, and debugging via devtools/logs.
Principles to keep while coding
Choose query keys that uniquely describe the data and parameters being cached.
Invalidate or update the relevant cache after mutations so UI state converges with the server.
Configure stale time and refetch behavior based on data freshness needs rather than copying defaults blindly.
Guided phase lab
Do these in order. Each step should end with evidence before the next one starts.
- 01
Install/configure one QueryClient at the correct client boundary.
- 02
Define a query-key factory that includes every parameter affecting the result.
- 03
Move task-list fetching into query options and observe loading/error/success/cache states.
- 04
Add task detail query and prove list/detail caches are distinct.
- 05
Implement create/update mutations and choose between invalidation and direct cache updates deliberately.
- 06
Add pagination/filter URL state and inspect how query keys prevent cross-filter cache corruption.
Failures you should know how to diagnose
Random query-key arrays scattered through components
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Copying query data into useState/useEffect
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Invalidating too broadly or not at all
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Treating staleTime as data lifetime/deletion
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Lessons
The source chapters come first. Every phase now ends with a guided build, a debugging workshop, and an independent challenge so reading turns into transferable skill.
Server State and Cache Mental Model
Server State and Cache Mental Model Know what TanStack Query owns - and what it does not Phase 22 - TanStack Query Volume 06 - Professional Frontend Four different kinds of frontend state A
QueryClient Provider and Important Defaults
QueryClient, Provider and Important Defaults Install v5.103.1 and build a predictable cache boundary Phase 22 - TanStack Query Volume 06 - Professional Frontend Install the two Phase 22 pack
Query Keys and Query Options
Query Keys and Query Options Design cache identity before writing hooks Phase 22 - TanStack Query Volume 06 - Professional Frontend A query key is the identity of cached data If two requests
useQuery Freshness Retries and Background Refetch
useQuery, Freshness, Retries and Background Refetch Read server state without recreating fetch lifecycle by hand Phase 22 - TanStack Query Volume 06 - Professional Frontend useQuery is a sta
Authenticated Task Queries End to End
Authenticated Task Queries End to End Connect query functions to the real protected NestJS API Phase 22 - TanStack Query Volume 06 - Professional Frontend Build an authenticated Task API cli
Pagination Filters and URL State
Pagination, Filters and URL State Let URL state describe the collection while Query handles remote data Phase 22 - TanStack Query Volume 06 - Professional Frontend The URL still owns filters
Mutations and Targeted Invalidation
Mutations and Targeted Invalidation Create and update remote state without manual refresh choreography Phase 22 - TanStack Query Volume 06 - Professional Frontend Writes belong in mutations
React Hook Form and Query Mutations
React Hook Form and Query Mutations Connect Phase 21 forms without merging unrelated state systems Phase 22 - TanStack Query Volume 06 - Professional Frontend React Hook Form and TanStack Qu
Next.js App Router Prefetch and Hydration
Next.js App Router Prefetch and Hydration Keep server rendering while gaining a live client cache Phase 22 - TanStack Query Volume 06 - Professional Frontend Preserve Server Components with
Authentication Cache Isolation and Privacy
Authentication, Cache Isolation and Privacy Treat logout as a data-boundary event, not just a navigation action Phase 22 - TanStack Query Volume 06 - Professional Frontend Cached data can be
Devtools Testing and Debugging
Devtools, Testing and Debugging Learn to diagnose cache behavior instead of fighting it Phase 22 - TanStack Query Volume 06 - Professional Frontend Devtools make the invisible cache visible
Hands On Lab and Phase Exit
Hands-On Lab and Phase Exit Prove that you understand server-state ownership before moving on Phase 22 - TanStack Query Volume 06 - Professional Frontend Hands-on lab: migrate the Task colle
Guided Build Lab
Purpose Use TanStack Query as the owner of asynchronous server state with deliberate query keys, freshness, invalidation, mutations, and pagination behavior.
Debugging Workshop
Goal Learn to diagnose this phase's failures systematically instead of reinstalling packages, changing random configuration, or copying a fix without understanding it.
Phase Synthesis & Independent Challenge
Mission Rebuild the capability of Phase 22 without following the exact code line-by-line. The goal is transfer: can you use the ideas when the variable names, UI, or route shape changes?
Mastery Capstone Lab
Integrate Phase 22 without following a transcript.
Exercises that increase independence
Install/configure one QueryClient at the correct client boundary.
Move task-list fetching into query options and observe loading/error/success/cache states.
reproduce and diagnose — Random query-key arrays scattered through components.
deliver TaskFlow query/mutation layer with stable keys, list/detail caching, invalidation, filters, and debugging via devtools/logs.
Can explain cache freshness vs garbage collection and Can design stable keys.
Do not leave the phase only because you reached the bottom
Phase exit criteria
Scenario and concept quiz
These questions test engineering choices rather than whether you remember a lesson title.
Reference files
Attempt the work first. Use snapshots to compare a checkpoint, not as a substitute for understanding.
reference/TaskFlow_TanStack_Query_Snapshot/QUERY_KEY_CATALOG.md
# TaskFlow query key catalog - ["tasks"] - Task resource family - ["tasks", "list", query] - one normalized collection view - ["tasks", "detail", taskId] - one Task detail Rules: 1. Include every variable used by queryFn that changes returned data. 2. Keep keys JSON-serializable. 3. Never include access/refresh tokens or secrets. 4. Use prefixes for intentional invalidation families.
reference/TaskFlow_TanStack_Query_Snapshot/README.md
# TaskFlow Phase 22 reference snapshot This snapshot demonstrates TanStack Query v5.103.1 integration with the existing Next.js App Router, Supabase-authenticated API client and NestJS Task API. It is a teaching reference, not a wholesale replacement for earlier phase files. Key rule: TanStack Query owns remote server-state lifecycle; URL state, form state, local UI state, authentication and backend business rules keep their existing owners.
reference/TaskFlow_TanStack_Query_Snapshot/apps/web/package.phase22.json
{
"dependencies": {
"@tanstack/react-query": "5.103.1"
},
"devDependencies": {
"@tanstack/react-query-devtools": "5.103.1"
}
}reference/TaskFlow_TanStack_Query_Snapshot/apps/web/src/app/(app)/tasks/page.phase22.tsx
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { getQueryClient } from "@/lib/query/get-query-client";
import { taskListOptions } from "@/lib/query/task-options";
import { normalizeTaskSearchParams } from "@/lib/tasks/normalize-task-search-params";
import { TasksScreen } from "@/components/tasks/tasks-screen";
export default async function TasksPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
const query = normalizeTaskSearchParams(await searchParams);
const queryClient = getQueryClient();
await queryClient.prefetchQuery(taskListOptions(query));
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<TasksScreen query={query} />
</HydrationBoundary>
);
}reference/TaskFlow_TanStack_Query_Snapshot/apps/web/src/app/providers.tsx
"use client";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { getQueryClient } from "@/lib/query/get-query-client";
export function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}reference/TaskFlow_TanStack_Query_Snapshot/apps/web/src/components/tasks/tasks-screen.tsx
"use client";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { taskListOptions } from "@/lib/query/task-options";
import type { TaskListQuery } from "@/lib/api/tasks";
import { TaskTable } from "./task-table";
import { EmptyState } from "@/components/ui/empty-state";
export function TasksScreen({ query }: { query: TaskListQuery }) {
const result = useQuery({
...taskListOptions(query),
placeholderData: keepPreviousData,
});
if (result.isPending) return <p>Loading tasks...</p>;
if (result.isError) return <p role="alert">Unable to load tasks.</p>;
if (result.data.data.length === 0) return <EmptyState title="No tasks found" />;
return (
<>reference/TaskFlow_TanStack_Query_Snapshot/apps/web/src/hooks/use-task-mutations.ts
"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { createTask, updateTask, type CreateTaskInput, type UpdateTaskInput } from "@/lib/api/task-mutations";
import { taskKeys } from "@/lib/query/task-keys";
export function useCreateTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: CreateTaskInput) => createTask(input),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
},
});
}
export function useUpdateTask(taskId: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: UpdateTaskInput) => updateTask(taskId, input),reference/TaskFlow_TanStack_Query_Snapshot/apps/web/src/lib/auth/sign-out-with-cache-clear.ts
"use client";
import type { QueryClient } from "@tanstack/react-query";
import { createClient } from "@/lib/supabase/client";
export async function signOutWithCacheClear(queryClient: QueryClient) {
const supabase = createClient();
await supabase.auth.signOut({ scope: "local" });
queryClient.clear();
window.location.assign("/sign-in");
}reference/TaskFlow_TanStack_Query_Snapshot/apps/web/src/lib/query/get-query-client.ts
import { environmentManager, QueryClient } from "@tanstack/react-query";
import { ApiClientError } from "@/lib/api/api-error";
function shouldRetry(failureCount: number, error: unknown) {
if (error instanceof ApiClientError && [400, 401, 403, 404, 409].includes(error.status)) {
return false;
}
return failureCount < 2;
}
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: { staleTime: 30_000, retry: shouldRetry },
mutations: { retry: 0 },
},
});
}
let browserQueryClient: QueryClient | undefined;reference/TaskFlow_TanStack_Query_Snapshot/apps/web/src/lib/query/task-keys.ts
import type { TaskListQuery } from "@/lib/api/tasks";
export const taskKeys = {
all: ["tasks"] as const,
lists: () => [...taskKeys.all, "list"] as const,
list: (query: TaskListQuery) => [...taskKeys.lists(), query] as const,
details: () => [...taskKeys.all, "detail"] as const,
detail: (id: string) => [...taskKeys.details(), id] as const,
};reference/TaskFlow_TanStack_Query_Snapshot/apps/web/src/lib/query/task-options.ts
import { queryOptions } from "@tanstack/react-query";
import { fetchTask, fetchTasks, type TaskListQuery } from "@/lib/api/tasks";
import { taskKeys } from "./task-keys";
export function taskListOptions(query: TaskListQuery) {
return queryOptions({
queryKey: taskKeys.list(query),
queryFn: () => fetchTasks(query),
staleTime: 30_000,
});
}
export function taskDetailOptions(id: string) {
return queryOptions({
queryKey: taskKeys.detail(id),
queryFn: () => fetchTask(id),
staleTime: 30_000,
});
}reference/TaskFlow_TanStack_Query_Snapshot/tests/tanstack-query-test-matrix.md
# Phase 22 test matrix - First Task list visit -> pending state, then successful table. - Return to a still-fresh page -> cached data reused without unnecessary fetch. - Let staleTime expire, refocus window -> background refetch while table remains visible. - Change page -> URL changes, query key changes, previous page remains as placeholder while next loads. - Change search/status -> page resets to 1 and a distinct list key is used. - Create Task -> mutation succeeds, Task list key family invalidated, active list refreshes. - Update Task -> detail cache receives saved Task and list family invalidates. - 401/403/409 -> query/mutation does not loop retries. - User A sign-out -> queryClient.clear(); User B sign-in starts with no User A cached Task data. - Devtools -> keys show no bearer token or secret values.