nuqs review
We measured nuqs 2.10.0 as a 25 KB minified, 9.2 KB gzipped browser import. It gives React query parameters a useState-like API, with parsers for numbers, booleans, dates, arrays, JSON, enums, and custom codecs. Adapters connect the same hooks to Next.js, plain React, Remix, React Router, and TanStack Router; server exports reuse parser definitions in loaders, caches, and link serializers. Current version 2.10.1 fixes first-mount URL state, discarded navigation recovery, and testing-adapter queue resets, while trimming useQueryStates code.
nuqs 2.10.0 installed in 4.8 seconds and produced a 9.2 KB gzipped browser build in our sandbox, with bundled types and no audit findings. It fits React products where URL state is a deliberate interface; local, sensitive, or deeply validated state belongs elsewhere.
We installed it
| Install | ✓ · 4.8s | 5 packages on disk · 2 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 9.2 KB | gzipped (25 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does nuqs install cleanly?
Yes. In a fresh container with an empty cache, npm install nuqs finished in 5 seconds, leaving 5 packages and 2 MB on disk. npm audit reported no known vulnerabilities.
How much does nuqs add to a browser bundle?
9.2 KB gzipped (25 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does nuqs work with both ESM and CommonJS?
Yes. Both import 'nuqs' and require('nuqs') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does nuqs include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
nuqs or use-query-params: which should you use?
use-query-params: Choose it when its codec model and router adapters already match an established React application. nuqs 2.10.0 installed in 4.8 seconds and produced a 9.2 KB gzipped browser build in our sandbox, with bundled types and no audit findings.
When should you not use nuqs?
The value is secret, private, large, or updated at animation speed. Query strings are visible in links and logs, have practical length limits, and are a poor store for rapid local state.
Use it if
- Filters, pagination, sorting, tabs, or map coordinates should survive refreshes and travel in a copied URL.
- Client hooks and server code should share one typed definition for parsing and serializing query keys.
- Several related parameters need one batched URL update with deliberate replace or push history behavior.
- Your app uses a listed Next.js, React SPA, Remix, React Router, or TanStack Router adapter.
- The value is secret, private, large, or updated at animation speed. Query strings are visible in links and logs, have practical length limits, and are a poor store for rapid local state.
- Parsing must enforce business rules by itself. The documentation distinguishes parsing from validation; parseAsInteger does not prove a positive range, and parseAsJson<T>() does not check that T at runtime.
- You cannot add a router adapter above the components. Version 2 requires one, and Next.js App Router, Pages Router, React Router versions, Remix, and plain React use different entry points.
- Every client update must immediately re-run server rendering. Shallow updates are the default, and browser History API writes are queued and rate-limited.
- TanStack Start is the target. The README labels the TanStack Router adapter experimental and explicitly excludes TanStack Start support.
Setup reality
We installed nuqs 2.10.0 in a fresh Node 22 Bookworm container. npm finished in 4.8 seconds, left 5 packages, and used 2 MB on disk. The package was 784 KB unpacked, with 1 direct dependency and 6 peer dependencies. npm audit reported 0 known vulnerabilities. It is ESM with an exports map, yet both require() and ESM import worked in our check. TypeScript declarations are bundled. A namespace browser import built to 25 KB minified and 9.2 KB gzipped.
The first setup choice is the adapter. Wrap the correct React tree with the import for Next App Router, Next Pages Router, plain React, Remix, or the exact React Router generation. Current peers allow React 18.2 or 19 and Next 14.2 or newer. In Next App Router, hooks belong in client components. Loaders, serializers, parsers, and server caches should come from nuqs/server so server files do not inherit client-only code.
A missing key returns null unless the parser has withDefault(), and that default is not inserted into the URL. Setting null removes the key. Query input remains untrusted after a parser succeeds: enforce ranges, allowed object shapes, and authorization rules separately. Version 2.10.1 now restores state after React discards a navigation render and fixes an initial render when the URL already contains a value, which matters most under concurrent rendering.
URL changes replace history by default. Choose history: 'push' only for states where the Back button should visit each change. Next.js updates are shallow unless shallow: false asks the server to run again. nuqs batches and rate-limits History API writes while updating hook state immediately, so await the setter promise when later code needs the flushed URL. High-frequency text fields should use the documented debounce or throttle controls instead of triggering server work for every keypress.
Patterns
Connect a Next.js App Router tree wrap-next-app-router
import { NuqsAdapter } from 'nuqs/adapters/next/app';
import type { ReactNode } from 'react';
export default function RootLayout({ children }: { children: ReactNode }) {
return <html><body><NuqsAdapter>{children}</NuqsAdapter></body></html>;
}Next.js Pages Router uses nuqs/adapters/next/pages. The two adapters are not interchangeable.
Store an input value in the URL bind-string-query
'use client';
import { useQueryState } from 'nuqs';
const [query, setQuery] = useQueryState('q');
return <input value={query ?? ''} onChange={(event) => setQuery(event.target.value)} />;Without a parser or default, the value is string or null. A present but empty q= value produces an empty string.
Give an integer parameter a default parse-page-number
import { parseAsInteger, useQueryState } from 'nuqs';
const [page, setPage] = useQueryState(
'page',
parseAsInteger.withDefault(1),
);withDefault(1) removes null from the state type but does not write page=1 when the key is absent.
Remove a parameter delete-query-key
const [page, setPage] = useQueryState(
'page',
parseAsInteger.withDefault(1),
);
await setPage(null);Passing null deletes the URL key and returns hook state to the parser's default value.
Make Back revisit a state change push-history-entry
const [tab, setTab] = useQueryState('tab', { history: 'push' });
await setTab('billing');History mode defaults to replace. push adds an entry and can make Back tedious if used on every keystroke.
Notify Next.js server components rerun-next-server
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useQueryState(
'q',
parseAsString.withOptions({ shallow: false, startTransition }),
);shallow is true by default. Setting it to false starts server work for each flushed update.
Limit frequent History API updates throttle-url-writes
import { throttle, useQueryState } from 'nuqs';
const [value, setValue] = useQueryState('value', {
limitUrlUpdates: throttle(340),
});Hook state changes immediately while the URL write waits; browser rate limits still apply.
Update related coordinates together batch-query-state
const [coords, setCoords] = useQueryStates({
lat: parseAsFloat.withDefault(45.18),
lng: parseAsFloat.withDefault(5.72),
});
await setCoords({ lat: 51.5, lng: -0.12 });One useQueryStates setter batches the provided keys into one URL update and accepts partial changes.
Reject ports outside the allowed range validate-custom-port
import { createParser } from 'nuqs';
const parseAsPort = createParser({
parse(value) {
const port = Number(value);
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
},
serialize: String,
});Return null for invalid input. A later withDefault() can then supply the application fallback.
Parse query state outside React hooks load-server-params
import { createLoader, parseAsInteger, parseAsString } from 'nuqs/server';
const loadSearch = createLoader({
q: parseAsString,
page: parseAsInteger.withDefault(1),
});
const values = loadSearch('?q=printer&page=2');A loader converts syntax but does not enforce application rules such as a maximum page number.
Build a typed link serialize-search-link
import { createSerializer, parseAsInteger, parseAsString } from 'nuqs/server';
const serialize = createSerializer({ q: parseAsString, page: parseAsInteger });
const href = serialize('/search?view=grid', { q: 'laser printer', page: 2 });A null property removes an existing key; an omitted property leaves the base URL value untouched.
Observe URL updates in a component test test-with-adapter
import { render } from '@testing-library/react';
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
const onUrlUpdate = vi.fn();
render(<Filters />, {
wrapper: withNuqsTestingAdapter({
searchParams: '?page=2',
onUrlUpdate,
}),
});Version 2.10.1 resets the testing adapter's URL update queue once per mount, preventing state from leaking between mounts.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| use-query-params | npm | Choose it when its codec model and router adapters already match an established React application. |
| query-string | npm | Choose it for framework-independent parsing and serialization when React state synchronization is unnecessary. |
| react-router-dom | npm | Use its built-in useSearchParams when a React Router application only needs string values and manual conversion. |
More web frontend guides
postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

