Skip to main content
URL State in the Next.js App Router: Type-Safe Search Params That Survive a Refresh

URL State in the Next.js App Router: Type-Safe Search Params That Survive a Refresh

July 18, 2026
Frontend Engineering
6 min read

Key takeaways

URL state stores UI state — filters, the active tab, a search query, pagination — in the URL query string instead of React `useState`, so a refresh, a shared link, or the browser back button restores exactly what the user saw.

In the Next.js App Router we read query state with `useSearchParams()` in a client component, or with the async `searchParams` prop on a server component, which in Next.js 15 is a Promise the server awaits before rendering.

We write query state with `router.replace()` and a `URLSearchParams` object, and we use `replace` rather than `push` so filter tweaks do not flood the browser back-button history.

The `nuqs` library provides a `useState`-shaped hook called `useQueryState`, with typed parsers such as `parseAsInteger` and `parseAsBoolean` that return typed values instead of raw strings.

A component that calls `useSearchParams()` must sit inside a React `<Suspense>` boundary during static rendering, or the Next.js production build fails.

When should we store state in the URL instead of useState?

Store state in the URL when the user expects it to survive a refresh, a shared link, or a bookmark. Filters, the active tab, a search term, sort order, and pagination all qualify. Keep state in `useState` when it is ephemeral to one session and one component, such as a hover state, whether a dropdown is open, or an unsaved form draft.

The test our team uses is a single question: if the user copies this URL and sends it to a colleague, should the colleague see the same view? If the answer is yes, that state belongs in the URL. URL state also removes a class of synchronization bugs, because there is one source of truth — the query string — instead of a `useState` value that can drift out of sync with the address bar.

How do we read search params in the Next.js App Router?

There are two ways, depending on where the code runs. In a client component we call the `useSearchParams()` hook from `next/navigation`, which returns a read-only `URLSearchParams` object, and we read a key from it with a fallback default such as `searchParams.get('status') ?? 'all'`.

In a server component we skip the hook entirely and read the `searchParams` prop that every page receives. In Next.js 15 that prop is a Promise, so the component awaits it and fetches the correct data before it renders a single byte. That server-side read is what enables real server-side filtering with no client fetch at all.

How do we write to the URL without triggering a full navigation?

We build a fresh `URLSearchParams` from the current params, set the target key on it, and call `router.replace()` with the pathname and the new query string. Seeding the new params from the existing ones preserves every other query key already on the page.

The detail that matters is using `router.replace()` rather than `router.push()` for filter and tab changes. `push` adds a history entry on every change, so the back button steps through each individual tweak instead of returning to the previous page, while `replace` keeps a single entry.

What is nuqs, and what does useQueryState add over useSearchParams?

`nuqs` is a type-safe search-params state manager for React that turns the read-plus-`router.replace` pattern into one `useState`-shaped hook. Its `useQueryState(key, parser)` returns a value-and-setter tuple, except the value lives in the URL rather than in component memory.

The parsers are the real gain: `parseAsInteger`, `parseAsBoolean`, `parseAsArrayOf`, `parseAsStringEnum`, and `parseAsIsoDateTime` convert the string in the URL into the type the component actually wants, and back again on write. Setting a value to `null` removes the key from the URL. In `nuqs` version 2 we wrap the app once in `<NuqsAdapter>` from `nuqs/adapters/next/app`, and batch several keys at once with `useQueryStates`.

Native searchParams or nuqs: which should we pick?

We pick native `useSearchParams` for one or two string params, and `nuqs` when there are many typed params or we want the boilerplate gone.

Native `useSearchParams` gives strings only, so we parse them ourselves, requires no setup, and writes through a manual `URLSearchParams` plus `router.replace`.

`nuqs` gives typed parsers for integers, booleans, arrays, enums, and dates, needs a one-time `<NuqsAdapter>` wrapper, exposes a `useState`-like tuple, batches many keys with `useQueryStates`, handles defaults through `withDefault` and `clearOnDefault`, and throttles rapid writes for us.

What breaks — Suspense, throttling, and stale server data?

The failure that costs teams time is the build aborting with the message that `useSearchParams()` should be wrapped in a suspense boundary. During static rendering, any component that reads `useSearchParams()` must sit inside a React `<Suspense>` boundary, because the params are only known at request time. Wrapping the reading component makes the build pass.

Two further edges are worth knowing. Writing to the URL on every keystroke spams both history and re-renders, so `nuqs` throttles URL updates by default and lets us raise `throttleMs`, whereas with native code we debounce the write ourselves. And whether a URL change re-runs server components depends on shallow routing: `nuqs` defaults to `shallow: true`, which updates the URL on the client without a server round-trip, so we set `shallow: false` when a server component filters through the `searchParams` prop and needs the new value to refetch.

FAQ

Q: When should we keep state in the URL versus useState?

A: Use the URL for state that should survive a refresh, a shared link, or a bookmark — filters, tabs, search, sort, and pagination. Use `useState` for ephemeral UI such as open menus, hover, or an unsaved form draft.

Q: Do we need nuqs, or is useSearchParams enough?

A: `useSearchParams` plus `router.replace` covers reading and writing. `nuqs` adds typed parsers, a `useState`-like API, multi-key batching, and throttling. Native is enough for one or two string params; nuqs pays off for many typed params.

Q: Why does the build fail with "useSearchParams() should be wrapped in a suspense boundary"?

A: During static rendering Next.js requires any component calling `useSearchParams()` to be inside a `<Suspense>` boundary, since the value is only known at request time. Wrap that component in `<Suspense>`.

Q: Should we use router.push or router.replace for filter changes?

A: Use `router.replace`. `push` adds a history entry per change, so the back button walks through every filter tweak, while `replace` keeps one entry and returns the user to the previous page.

Q: Does updating the URL re-run our server components?

A: With native `router.replace`, yes — it re-renders the route's server components. With `nuqs` it depends on the `shallow` option: the default `shallow: true` is client-only, and `shallow: false` triggers the server refetch.