The full state matrix, URL state with nuqs, per-request server state with React cache(), cookie-based server state, Zustand with RSC, and the taint API for preventing accidental secret exposure.
A-8 — State Across the Network Boundary
Who this is for: Architects wrestling with the App Router's most counterintuitive constraint — Server Components can't hold state, Client Components can't run on the server, and data flows in one direction. This module is about the patterns that resolve these tensions: URL as state, server-to-client prop threading, context placement, and the patterns that keep your component tree sane when half of it lives on the server.
The Core Constraint
In the App Router, state lives on one side of the boundary or the other. There's no "shared" state in the traditional React sense.
Data flows from server to client through props. It cannot flow the other way — a Client Component cannot pass state back to a Server Component at runtime (it can submit mutations through Server Actions, which re-render Server Components, but that's not state passing).
This constraint forces a specific architectural question: where does this state actually belong?
URL as the Universal State Store
The URL is the only state that's simultaneously accessible on the server and the client. A Server Component can read searchParams. A Client Component can read useSearchParams(). Both see the same value.
This makes the URL the natural home for state that affects server rendering — filters, sort order, pagination, selected tabs:
The user clicks a filter → URL changes → Server Component re-renders with the new filter → fresh database query. No client-side filtering, no state synchronisation, no stale UI. The URL is the single source of truth.
The nuance with useSearchParams: Accessing useSearchParams() in a Client Component that's inside a Server Component causes the Client Component to suspend until searchParams are available. Wrap it in a <Suspense> boundary if the filter UI shouldn't block rendering.
Threading Props from Server to Client
The most common mistake with the App Router is trying to share state between a deeply-nested Server Component and a distant Client Component. The correct pattern: hoist the data fetch to the nearest Server Component ancestor, then thread it down as props.
The anti-pattern is treating Server Components like you'd treat a hook — calling auth() in every Server Component that needs the session. It's wasteful. The data flows in one direction; fetch it once at the top and pass it down.
Context Across the RSC Boundary
React context doesn't cross the RSC boundary. A createContext() in a Server Component cannot be consumed by a Client Component — and vice versa.
The pattern for sharing state with many Client Components without deep prop drilling:
The critical point: children passed to UserProvider can still be Server Components. The Client Component wrapper doesn't "infect" its children with client-side rendering — children props passed from a Server Component to a Client Component remain Server Components. Only components imported by Client Components become Client Components.
useFormStatus and useActionState — Form-Level State
For form state that's tightly coupled to a Server Action, React provides two hooks:
useFormStatus — reads the status of the nearest <form>:
useFormStatus must be inside the form — it reads the form's pending state from React's context. It can't be in the same component as the <form> tag. This is why it's typically in a separate SubmitButton component.
useActionState — manages the state returned by a Server Action:
useActionState is the replacement for the deprecated useFormState. The signature is identical — it's purely a rename.
Zustand and Global Client State
For state that's genuinely global to the client application — theme, notification count, shopping cart — a state management library like Zustand integrates cleanly with the App Router.
The key: initialise Zustand stores from server-fetched data, not from independent client fetches.
This pattern: server fetches the authoritative cart data, passes it to the client store as initial state, client store handles subsequent optimistic updates. The cart in the Zustand store is the "fast" version; after any mutation, revalidatePath ensures the next full page navigation fetches fresh data from the server.
The cookies() / headers() Boundary
A Server Component can call cookies() and headers(). A Client Component cannot — it has no access to the request. This creates a real architectural constraint: personalisation data (user preferences stored in cookies, A/B test assignments in headers) must be fetched server-side and passed to the client.
The Server Component reads the cookies and passes the values to Client Component providers as props. The Client Component providers make the values available via context without needing to re-read the cookies (which they can't do).
Where We Go From Here
A-9 moves to the edge — feature flags, geo-routing, and the architecture of global applications that serve different content to different users at the CDN layer. With A-8's understanding of how state flows through the component tree, A-9 explains how to use Middleware and edge compute to make routing decisions before the page even renders.
Why should you avoid using useState for filter options like "sort by price" in a Next.js App Router application?
Which of the following statements about passing state from Server Components to Client Components via React context is true?
What is the correct way to initialize a global client state library like Zustand with server-fetched data in the App Router?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
Sign in & RegisterDiscussion
0Join the discussion