# URL state management in Next.js App Router

> Keep typed state in the Next.js URL: searchParams from Server Components, no Suspense boundary, prerendering kept, layouts via proxy.ts, shallow history updates. Guide and FAQ for the state-in-url useUrlState hook.

Canonical: <https://state-in-url.dev/nextjs>. Library reference: <https://state-in-url.dev/llms.txt>. Comparison: <https://state-in-url.dev/vs/nuqs>.

state-in-url keeps typed state in the query string on Next.js 14, 15 and 16: one useUrlState hook per feature, no adapter, no provider, no Suspense boundary. This page covers what is specific to the App Router — Server Components, prerendering, layouts and history.

## Forward searchParams from the server page

A Server Component page receives searchParams — a Promise since Next.js 15. Await it and pass the object into the client component, which hands it to the hook. The first server render then shows the URL’s values instead of the defaults, so there is no flash and no hydration warning.

```tsx
// app/jobs/page.tsx  (Server Component)
import { JobsList } from './JobsList';

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  // A Promise since Next.js 15; a plain object in 14
  return <JobsList searchParams={await searchParams} />;
}
```

```tsx
// app/jobs/JobsList.tsx
'use client';
import { useUrlState } from 'state-in-url/next';

// Outside the component: sharing is keyed by object identity
const JOBS_STATE = { q: '', page: 1, remote: false, tags: [] as string[] };

export function JobsList({ searchParams }: { searchParams: object }) {
  const { urlState, setUrl } = useUrlState(JOBS_STATE, { searchParams });

  return (
    <input
      value={urlState.q}
      onChange={(ev) => setUrl({ q: ev.target.value, page: 1 })}
    />
  );
}
```

## No Suspense boundary, prerendering kept

The hook never calls useSearchParams, so a component using it doesn't need wrapping in Suspense and doesn't opt its page out of prerendering — PPR and cacheComponents included. It reads the URL directly and follows every later change, including a history.pushState from code that knows nothing about it. A prerendered page still renders the defaults, because there is no query string at build time — render a route dynamically when a shared link must be right on first paint.

## Layouts: decode the query string from a header

Server layouts never receive searchParams. Copy the query string into a request header in proxy.ts (middleware.ts still works as a deprecated alias) and decode it in the layout with decodeState and the same default-state object — the result is typed exactly like urlState on the client.

```tsx
// proxy.ts  (middleware.ts before Next.js 16)
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';

export function proxy(request: NextRequest) {
  const sp = (request.url.includes('_next') ? '' : request.url).split('?')[1] ?? '';
  const headers = new Headers(request.headers);
  headers.set('searchParams', sp);
  return NextResponse.next({ request: { headers } });
}
```

```tsx
// app/jobs/layout.tsx  (Server Component)
import { headers } from 'next/headers';
import { decodeState } from 'state-in-url/encodeState';
import { JOBS_STATE } from './jobsState';

export default async function Layout({ children }: { children: React.ReactNode }) {
  const sp = (await headers()).get('searchParams') ?? '';
  const initial = decodeState(sp, JOBS_STATE); // typed like urlState

  return <>{/* use `initial` */}{children}</>;
}
```

## History, shallow updates and scroll

setUrl replaces the current history entry by default, so typing does not pile up entries; pass replace: false to push one. Updates go through the History API — no server round trip and no _rsc request per keystroke. Pass useHistory: false to go through the Next.js router instead, when the server should re-render on every change. scroll is false by default.

## Fast inputs: render now, write the URL later

For text fields and sliders, update with setState on every change and call setUrl() with no arguments on blur or after a debounce. The component re-renders immediately; the URL is written once, with content-based diffing, so calling it repeatedly is safe.

```tsx
const { urlState, setState, setUrl } = useJobsState();

// Render now, write the URL once the field is left
<input
  value={urlState.q}
  onChange={(ev) => setState({ q: ev.target.value })}
  onBlur={() => setUrl()}
/>

setUrl({ page: 2 });                     // replaces the history entry (default)
setUrl({ page: 2 }, { replace: false }); // pushes a new one — Back returns to page 1
setUrl({ page: 2 }, { scroll: true });   // scroll to top, off by default
```

## Next.js URL state — frequently asked questions

### How do I keep state in the URL in Next.js App Router?

Define a default-state object outside the component, wrap useUrlState from state-in-url/next in a small hook, and call that hook in any client component. urlState is the typed current value and setUrl writes a partial into the query string. Pass the page’s searchParams prop in so the server render is already correct.

### Does useSearchParams need a Suspense boundary, and does state-in-url?

Next’s useSearchParams opts a statically rendered route into client rendering up to the nearest Suspense boundary, and the build fails without one. state-in-url never calls it: it reads searchParams on the server and window.location on the client, so no boundary is needed and prerendering, PPR included, is kept.

### How do I read URL state in a Server Component?

Pages get it as the searchParams prop — await it and either forward it to the client hook or decode it on the server with decodeState and the same default object. Layouts do not receive searchParams; expose the query string through a header set in proxy.ts and decode that.

### Does updating the URL re-render the page on the server?

Not by default. setUrl updates through the History API, so nothing is fetched and no _rsc request is made. When the server should see the new state — say, to refetch a list in a Server Component — pass useHistory: false so updates go through the Next.js router and the route re-renders.

### Is state-in-url a nuqs alternative for Next.js?

Yes. Both keep typed state in the query string; state-in-url takes one object with nested values and dates preserved, needs no adapter component and no per-key parser, and never touches useSearchParams. nuqs fits better when each value should be its own hand-readable query param. See the full comparison.

### Which Next.js versions are supported?

Next.js 14, 15 and 16 on the App Router, including the async searchParams introduced in 15 and cacheComponents with PPR in 16. Other setups can use the framework-agnostic encodeState and decodeState helpers with the router of their choice.
