Module A-8·24 min read

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.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

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.

text

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:

tsx
tsx

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.

nuqs — Type-Safe URL State Without Hand-Rolling It

The ProductFilters example above works, but it doesn't scale past a couple of string params. The moment you need a number, a boolean, an array, or a default value, you're writing (and re-writing, in every component) boilerplate around URLSearchParams: parsing strings to numbers, guarding against null, JSON-encoding arrays, remembering to call .toString() correctly. Get any of that wrong and you get a filter that silently resets, or a page number that's NaN.

nuqs is a library built specifically for this problem — type-safe search-param state that behaves like useState, but backed by the URL instead of component memory.

tsx

What this buys you over raw useSearchParams() plus manual URLSearchParams writes:

  • Parsers, not string juggling. parseAsInteger, parseAsBoolean, parseAsIsoDateTime, parseAsStringEnum, parseAsArrayOf, and others handle the serialisation and parsing (and the edge cases — missing param, malformed value) for you, in both directions.
  • Defaults baked into the type. .withDefault(1) means page is a number, never number | null, everywhere you read it.
  • Batched updates. useQueryStates writes multiple params in a single URL update instead of triggering a navigation per param, which matters once a filter panel controls more than one query key at a time.
  • The Server Component side stays exactly the same. nuqs only changes how the client reads and writes URL state; the Server Component still reads searchParams the normal Next.js way (nuqs also ships a createSearchParamsCache / server-side loader helper for keeping the server-side parsing in sync with the same parser definitions — verify the exact export name against the version you install, since this part of the library's API has moved between major versions).

Reach for raw useSearchParams() when you're reading a single, simple string param and don't need to write to it. Reach for nuqs the moment you're managing more than one param, need a non-string type, or need default-value semantics — hand-rolling that logic across a handful of filter components is exactly the kind of repetitive, easy-to-get-subtly-wrong code a small library exists to remove.


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.

tsx

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.