Course navigationPhase 22
Volume 06 · Phase 22

TanStack Query

Use TanStack Query as the owner of asynchronous server state with deliberate query keys, freshness, invalidation, mutations, and pagination behavior.

16lessons + labs
5klesson words
12source references
What you will ship

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.

Figure 22.1 — Where this phase lives in TaskFlow
component
query key/options
cache
HTTP fetch
server
cache update/invalidation
render
Trace the flow left-to-right for implementation and right-to-left for debugging. At every arrow, ask what data crosses the boundary and what proves it is trustworthy.
Instructor mental model

Principles to keep while coding

01

TanStack Query owns asynchronous server state; it is not a replacement for every kind of React state.

02

Choose query keys that uniquely describe the data and parameters being cached.

03

Invalidate or update the relevant cache after mutations so UI state converges with the server.

04

Configure stale time and refetch behavior based on data freshness needs rather than copying defaults blindly.

Code-along path

Guided phase lab

Do these in order. Each step should end with evidence before the next one starts.

  1. 01

    Install/configure one QueryClient at the correct client boundary.

  2. 02

    Define a query-key factory that includes every parameter affecting the result.

  3. 03

    Move task-list fetching into query options and observe loading/error/success/cache states.

  4. 04

    Add task detail query and prove list/detail caches are distinct.

  5. 05

    Implement create/update mutations and choose between invalidation and direct cache updates deliberately.

  6. 06

    Add pagination/filter URL state and inspect how query keys prevent cross-filter cache corruption.

Debugging studio

Failures you should know how to diagnose

Failure drill 1

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.

Failure drill 2

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.

Failure drill 3

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.

Failure drill 4

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.

Watch-order style

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.

01

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

2 min
02

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

2 min
03

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

2 min
04

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

2 min
05

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

2 min
06

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

2 min
07

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

2 min
08

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

2 min
09

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

2 min
10

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

2 min
11

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

2 min
12

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

2 min
LAB

Guided Build Lab

Purpose Use TanStack Query as the owner of asynchronous server state with deliberate query keys, freshness, invalidation, mutations, and pagination behavior.

3 min
LAB

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.

2 min
LAB

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?

2 min
LAB

Mastery Capstone Lab

Integrate Phase 22 without following a transcript.

2 min
Practice ladder

Exercises that increase independence

Guided

Install/configure one QueryClient at the correct client boundary.

Practice

Move task-list fetching into query options and observe loading/error/success/cache states.

Debug

reproduce and diagnose — Random query-key arrays scattered through components.

Independent

deliver TaskFlow query/mutation layer with stable keys, list/detail caching, invalidation, filters, and debugging via devtools/logs.

Explain-back

Can explain cache freshness vs garbage collection and Can design stable keys.

Mastery gate

Do not leave the phase only because you reached the bottom

Phase exit criteria

Knowledge check

Scenario and concept quiz

These questions test engineering choices rather than whether you remember a lesson title.

0/0 correct · 0/8 answered
01Which practice best matches Phase 22's engineering goal?
02Which practice best matches Phase 22's engineering goal?
03Which practice best matches Phase 22's engineering goal?
04Which practice best matches Phase 22's engineering goal?
05You are working through the Phase 22 lab. Which next action is the most disciplined step?
06You are working through the Phase 22 lab. Which next action is the most disciplined step?
07You are working through the Phase 22 lab. Which next action is the most disciplined step?
08You are working through the Phase 22 lab. Which next action is the most disciplined step?
Source snapshot

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.