Put shareable navigation state in the URL, server state in TanStack Query, and local interaction state near the component that owns it.
Course navigationPhase 23⌄
Professional Frontend State Architecture
Choose the correct owner for URL state, server state, local UI state, forms, and global client state instead of duplicating everything.
Phase outcome and project increment
Learning outcome
Choose the correct owner for URL state, server state, local UI state, forms, and global client state instead of duplicating everything.
Concrete deliverable
A state ownership map for TaskFlow plus refactored examples removing redundant synchronized state.
Principles to keep while coding
Avoid duplicating the same piece of state across URL, React state, and query cache.
Derive values when possible instead of synchronizing multiple mutable copies.
Choose state ownership by lifecycle and source of truth, not by whichever library is fashionable.
Guided phase lab
Do these in order. Each step should end with evidence before the next one starts.
- 01
Inventory every state variable on one TaskFlow screen and classify it: server, URL, form, local UI, or cross-cutting client.
- 02
Move filter/search/page state that should survive/share navigation into URL search params.
- 03
Remove local copies of TanStack Query data and derive filtered/presentational values during render.
- 04
Keep ephemeral dialog/menu state at the smallest component that owns it.
- 05
Evaluate whether any global store is actually needed; write the requirement before adding one.
- 06
Create a diagram showing source of truth and allowed data flow for the screen.
Failures you should know how to diagnose
useEffect syncing query data into local state
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Global store used for all state categories
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
URL and React state disagreeing about filters
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Storing derived values that can be computed
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.
State Taxonomy and Ownership Decision Framework
State Taxonomy and Ownership Decision Framework Volume 06 - Professional Frontend | Phase 23 The first question is ownership, not API choice When developers ask "Should this use Context or Z
Avoid Redundant Duplicate and Synchronized State
Avoid Redundant, Duplicate and Synchronized State Volume 06 - Professional Frontend | Phase 23 The most expensive state is copied state Every duplicate introduces a synchronization problem.
Local UI State useState and Component Ownership
Local UI State, useState and Component Ownership Volume 06 - Professional Frontend | Phase 23 Keep ephemeral interaction state local Mobile navigation visibility, a disclosure panel, a tempo
URL State Filters Pagination and Deep Linkability
URL State: Filters, Pagination and Deep Linkability Volume 06 - Professional Frontend | Phase 23 The URL is a state container Phase 15 already made Task filters, sorting, and pagination part
Form State vs Server State
Form State vs Server State Volume 06 - Professional Frontend | Phase 23 A form is an intentional divergence from the server This is one of the few places where a local copy is not accidental
Auth Session Workspace and Cross Tree Context
Auth, Workspace and Cross-Tree Context Volume 06 - Professional Frontend | Phase 23 Context is a delivery mechanism, not automatically the owner Context is useful when many descendants need
useReducer for Complex Local Transitions
useReducer for Complex Local Transitions Volume 06 - Professional Frontend | Phase 23 Reducer is not a global store useReducer is valuable when one local feature has several related transiti
Derived State Selectors and Computation
Derived State, Selectors and Computation Volume 06 - Professional Frontend | Phase 23 If it can be computed, prefer computing it Derived state is information that can be calculated from auth
Server Client Boundaries and State Placement
Server/Client Boundaries and State Placement Volume 06 - Professional Frontend | Phase 23 Client state creates a client boundary In the App Router, Server Components are still the default ar
When Context or Global State is Actually Justified
When Context or Global State Is Actually Justified Volume 06 - Professional Frontend | Phase 23 Do not ban global state; earn it After moving server state to TanStack Query, forms to React H
State Testing Debugging and Common Failures
State Testing, Debugging and Common Failures Volume 06 - Professional Frontend | Phase 23 Debug ownership before debugging values When a screen shows stale or contradictory information, ask
Hands On Lab and Phase Exit
Hands-On Lab and Phase Exit Volume 06 - Professional Frontend | Phase 23 Lab 1 - State inventory Create an inventory of every stateful value on the Tasks screen. For each value, record categ
Guided Build Lab
Purpose Choose the correct owner for URL state, server state, local UI state, forms, and global client state instead of duplicating everything.
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 23 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 23 without following a transcript.
Exercises that increase independence
Inventory every state variable on one TaskFlow screen and classify it: server, URL, form, local UI, or cross-cutting client.
Remove local copies of TanStack Query data and derive filtered/presentational values during render.
reproduce and diagnose — useEffect syncing query data into local state.
deliver A state ownership map for TaskFlow plus refactored examples removing redundant synchronized state.
Can classify state before choosing library and Can remove duplicate state.
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_State_Architecture_Snapshot/README.md
# TaskFlow Phase 23 Reference Snapshot This snapshot demonstrates state ownership decisions. It is intentionally small and does not add a global state library. Owners: - TanStack Query: server state - Next.js URL: filters/pagination/shareable navigation - React Hook Form: unsaved form drafts - Supabase Auth: authentication session - React local state/reducer: ephemeral UI interaction - Focused Context: only stable cross-tree convenience values
reference/TaskFlow_State_Architecture_Snapshot/SOURCES.md
# Sources - React - Sharing State Between Components: https://react.dev/learn/sharing-state-between-components - Single source of truth; lift shared state to the closest common owner. - React - Managing State: https://react.dev/learn/managing-state - Avoid redundant/duplicate state; structure state intentionally. - Next.js - useSearchParams: https://nextjs.org/docs/app/api-reference/functions/use-search-params - URL query state in the App Router. - TanStack Query - Query State / Cache: https://tanstack.com/query/latest/docs/framework/react/reference/interfaces/QueryState - Server-state cache is separate from local UI state. - TanStack Query - SSR/Hydration: https://tanstack.com/query/latest/docs/framework/react/guides/ssr - Request-scoped QueryClient and cache isolation.
reference/TaskFlow_State_Architecture_Snapshot/STATE_OWNERSHIP_MATRIX.md
# TaskFlow State Ownership Matrix | Value | Owner | Why | Reset / lifetime | |---|---|---|---| | Task collection | TanStack Query | Remote server state | Cache freshness/auth lifecycle | | status/search/page | URL | Shareable navigation | Route change | | Task edit draft | React Hook Form | Intentional unsaved divergence | reset/cancel/success | | Supabase session | Supabase Auth | Identity provider owns session | logout/expiry | | mobile menu open | local useState | Ephemeral shell interaction | close/unmount | | selected Task rows | local reducer | Feature interaction state | clear/filter/page change | | current workspace slug | URL route scope | Reconstructable tenant scope | navigation | | resolved workspace metadata | focused Context | convenient deep distribution | route/workspace change | | completed count | derived | Calculated from authoritative result | render |
reference/TaskFlow_State_Architecture_Snapshot/apps/web/src/components/tasks/anti-pattern-copy-query-data.tsx
// Anti-pattern kept only as documentation. Do not use in TaskFlow.
const tasksQuery = useQuery(taskListOptions(query))
const [tasks, setTasks] = useState<Task[]>([])
useEffect(() => {
if (tasksQuery.data) setTasks(tasksQuery.data.data)
}, [tasksQuery.data])
// Prefer:
const tasks = tasksQuery.data?.data ?? []reference/TaskFlow_State_Architecture_Snapshot/apps/web/src/components/tasks/task-selection.reducer.ts
export type SelectionState = {
ids: Set<string>
}
export type SelectionAction =
| { type: "toggle"; id: string }
| { type: "replace"; ids: string[] }
| { type: "clear" }
export function selectionReducer(
state: SelectionState,
action: SelectionAction,
): SelectionState {
switch (action.type) {
case "toggle": {
const ids = new Set(state.ids)
ids.has(action.id) ? ids.delete(action.id) : ids.add(action.id)
return { ids }
}
case "replace":reference/TaskFlow_State_Architecture_Snapshot/apps/web/src/components/tasks/tasks-client-region.tsx
"use client"
import { useReducer } from "react"
import { useQuery } from "@tanstack/react-query"
export function TasksClientRegion({ query }: { query: TaskListQuery }) {
const tasksQuery = useQuery(taskListOptions(query))
const [selection, dispatch] = useReducer(selectionReducer, { ids: new Set<string>() })
// Server rows are rendered directly from the query cache.
const tasks = tasksQuery.data?.data ?? []
const completedCount = tasks.filter(task => task.status === "DONE").length
return (
<TasksView
tasks={tasks}
completedCount={completedCount}
selectedIds={selection.ids}
onToggle={(id) => dispatch({ type: "toggle", id })}reference/TaskFlow_State_Architecture_Snapshot/apps/web/src/components/workspace/workspace-context.tsx
"use client"
import { createContext, useContext } from "react"
export type WorkspaceSummary = {
id: string
slug: string
name: string
}
const WorkspaceContext = createContext<WorkspaceSummary | null>(null)
export function WorkspaceProvider({
workspace,
children,
}: {
workspace: WorkspaceSummary
children: React.ReactNode
}) {
return (reference/TaskFlow_State_Architecture_Snapshot/apps/web/src/lib/state/task-list-query.ts
export type TaskListQuery = {
page: number
pageSize: number
search?: string
status?: string
priority?: string
}
export function normalizeTaskListQuery(
searchParams: URLSearchParams,
): TaskListQuery {
return {
page: Math.max(1, Number(searchParams.get("page") ?? 1)),
pageSize: Math.min(100, Math.max(1, Number(searchParams.get("pageSize") ?? 20))),
search: searchParams.get("search")?.trim() || undefined,
status: searchParams.get("status") || undefined,
priority: searchParams.get("priority") || undefined,
}
}reference/TaskFlow_State_Architecture_Snapshot/tests/state-architecture-test-matrix.md
# Phase 23 State Architecture Test Matrix 1. Change status filter -> URL changes, page resets to 1. 2. Refresh -> same filter/page reconstructs. 3. Back/Forward -> collection follows browser history. 4. Background query refetch -> Task table updates without a copied local array. 5. Dirty edit form -> query refetch does not overwrite unsaved draft. 6. Successful save -> form reset + relevant query cache updated/invalidated. 7. Mobile menu -> local interaction only; no global store. 8. Logout -> Query cache cleared; previous user server data not shown. 9. Workspace route change -> resolved workspace Context updates from route/server owner. 10. Derived completed count -> changes automatically with task data, no effect synchronization.