# state-in-url vs nuqs

> Honest state-in-url vs nuqs comparison: setup, state shape, nested objects, dates and bundle size — plus TanStack Router, use-query-params and plain useSearchParams, with a live demo.

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

Searching for a nuqs alternative? Both keep typed state in the query string; they differ in how much you set up and what a value can be.

| What | state-in-url | nuqs |
| --- | --- | --- |
| Setup | None — import the hook and go | Adapter component wraps the app |
| State shape | One typed object, like React.useState | Per-key values, a parser declared for each |
| Reuse across components | Wrap the hook once — every component shares the state, no props | Extract your own hook around the parser map |
| Nested objects and arrays | Built in — structure and types preserved | JSON parser plus your own runtime validator |
| Dates | Preserved automatically | Built-in parser, declared per key |
| Size, full import | ~2.9 KB gzipped | ~6.7 KB gzipped |
| Runtime dependencies | None | One |
| Routers | Next.js, React Router v6/v7, Remix, plain JS helpers | Next.js, React Router, Remix, TanStack Router, plain React |

Sizes: whole-library import, esbuild minify + gzip, measured August 2026 against nuqs 2.10.1.

nuqs is a fine library — reach for it when you want each value as its own readable query param, or you are on TanStack Router. Reach for state-in-url when you want a whole typed object in the URL with zero setup.

## The same feature, in both

A filter panel: a search string, a page number, a tag list and a date. nuqs declares a parser per key and wires an adapter at the root; state-in-url takes the object and wraps it in one reusable hook.

nuqs:

```tsx
// app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <NuqsAdapter>{children}</NuqsAdapter>
      </body>
    </html>
  );
}
```

```tsx
'use client';
import {
  useQueryStates,
  parseAsString,
  parseAsInteger,
  parseAsArrayOf,
  parseAsIsoDateTime,
} from 'nuqs';

export const Filters = () => {
  const [filters, setFilters] = useQueryStates({
    q: parseAsString.withDefault(''),
    page: parseAsInteger.withDefault(1),
    tags: parseAsArrayOf(parseAsString).withDefault([]),
    since: parseAsIsoDateTime,
  });

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

state-in-url:

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

export const filters = {
  q: '',
  page: 1,
  tags: [] as string[],
  since: undefined as Date | undefined,
};

// One reusable hook = the whole API for this feature
export const useFilters = () => useUrlState(filters);

export const SearchBox = () => {
  const { urlState, setUrl } = useFilters();

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

export const ActiveTags = () => {
  // Same state, another component - no props, no context
  const { urlState } = useFilters();

  return <>{urlState.tags.join(', ')}</>; // tags is still string[]
};
```

That one custom hook is the whole API for the feature: every component that calls it shares the same typed state — the tag list stays an array, the date comes back a real Date object. No props, no context, no per-key wiring.

## Setup and boilerplate

nuqs plugs into your router through an adapter component wrapped around the app, and each piece of state declares its parser. state-in-url ships a hook per router — import the one that matches, hand it a default-state object, done. Nothing wraps anything.

### Next.js, SSR and prerendering

On the App Router, state-in-url never calls useSearchParams, so components using it need no Suspense boundary and their pages keep prerendering — PPR included. Server components read the same state through the searchParams prop, forwarded as-is.

### Migrating from nuqs

Most migrations are mechanical: gather one feature’s keys into a single default-state object, drop the parser declarations — plain typed values carry the same information — and replace the per-key setters with one setter that takes a partial. Each top-level field still maps to its own query parameter.

## How the other options compare

nuqs is not the only alternative. The same job — typed state in the query string — is also covered by router built-ins and older libraries, each with a different trade.

| Library | Setup | Nested objects and dates | Size | Pick it when |
| --- | --- | --- | --- | --- |
| state-in-url | None — import the hook | Preserved automatically, types included | ~2.9 KB gzip, zero deps | You want one typed object with zero setup on Next.js, React Router or Remix |
| nuqs | Adapter component, parser per key | JSON parser plus your own validator | ~6.7 KB gzip, one dep | You want each value as its own readable query param |
| TanStack Router | validateSearch on each route | JSON-first for objects and arrays; dates need custom serialization | Built into the router | You are on TanStack Router — use what it ships |
| use-query-params | Provider plus a router adapter, param config per key | Via a JSON param type, loosely typed | ~4.4 KB gzip plus serialize-query-params | A codebase already built on it |
| useSearchParams | None — built into the router | Strings only — parsing, typing and defaults are all yours | 0 KB | One or two flat string params, no library worth it |

## Frequently asked questions

### Is state-in-url a good nuqs alternative?

Yes, when you want a whole typed object in the URL with zero setup: no adapter component, no per-key parsers, and nested objects and dates preserved automatically. nuqs remains the better pick when you want each value as its own readable query param, or you are on TanStack Router.

### Which is smaller, state-in-url or nuqs?

Measured with esbuild (minify + gzip, whole-library import) in August 2026, state-in-url is ~2.9 KB with zero runtime dependencies; nuqs 2.10.1 is ~6.7 KB with one dependency. Importing a subset shrinks both.

### Does state-in-url need an adapter or provider?

No. Each router has its own entry point — import the matching hook, pass it a default-state object, and it works. There is no adapter component to wrap the app and no context provider to configure.

### How hard is it to migrate from nuqs to state-in-url?

Usually mechanical: gather one feature’s keys into a single default-state object, drop the parser declarations, and replace the per-key setters with one setter that takes a partial. Each top-level field still maps to its own query parameter.

### What about TanStack Router search params?

If you are on TanStack Router, use what it ships: JSON-first search params validated per route with validateSearch. state-in-url and nuqs matter when your router is Next.js, React Router or Remix, where typed search params are not built in.
