Typed state, living in the URL
useUrlState is React state that writes itself to the query string. Objects, arrays and dates keep their types, every state is a shareable link, and it survives reloads — no providers, no boilerplate.
- ~2 KB gzipped
- zero dependencies
- TypeScript-first
- Next.js / react-router / Remix
- MIT
npm i state-in-urlstate-in-url vs nuqs
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.
// app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app';
export default function RootLayout({ children }) {
return (
<html>
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
);
}'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 })}
/>
);
};'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.
