# nuqs: URL query state for React, one adapter per router, and shallow updates by default

> nuqs is a TypeScript library that turns URL query parameters into typed React state, with framework adapters, built-in parsers, and history control. It is narrow, well maintained, and quietly opinionated: updates are shallow by default, hooks are client-only, and TanStack Start is not covered yet.

**47ng/nuqs** — Type-safe search params state manager for React frameworks - Like useState, but stored in the URL query string.

- Repository: https://github.com/47ng/nuqs
- Website: https://nuqs.dev
- Stars: 10,868 · Forks: 296
- Language: TypeScript
- License: MIT
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/47ng-nuqs

## The adapter subpath you import decides which router owns the query string

nuqs does not read the URL on its own. You pick the framework's adapter and wrap the tree in it, and that wrapper is what connects the hooks to a router's history. There is a separate import path for each case: nuqs/adapters/next/app for the Next.js app router, nuqs/adapters/next/pages for the pages router, nuqs/adapters/react for a plain SPA built with Vite or create-react-app, nuqs/adapters/remix, nuqs/adapters/react-router/v6, nuqs/adapters/react-router/v7, nuqs/adapters/react-router/v8, and nuqs/adapters/tanstack-router. Each of those is its own entry in the package, so the framework you run is baked into your import line and into your bundle. In the app router the wrapper goes in the root layout around the children; on the pages router it wraps the Component together with its page props. The documentation is blunt about the requirement: you will need to wrap your React component tree with an adapter for your framework. Skip the wrapper and the hooks have no router to read from or write to.

Getting the code itself is a single line, and the same library is published for six package managers:

```shell
npm install nuqs
pnpm add nuqs
yarn add nuqs
bun add nuqs
deno add nuqs
vlt install nuqs
```

Behind that install sits a pnpm and turbo monorepo whose root package is private and whose published code lives under packages/, so what you read on GitHub is not what npm serves. The work is coordinated through turbo run build and turbo run test, with knip and sherif as the unused-code and dependency linters.

## React Router v6, v7 and v8 are three entry points, and v6 uses a different package name

Three of those adapter paths exist for React Router alone, and they are not interchangeable. The v6 adapter is imported from nuqs/adapters/react-router/v6 and its version note reads react-router-dom@^6. The v7 adapter comes from nuqs/adapters/react-router/v7 and expects react-router@^7, and the v8 adapter from nuqs/adapters/react-router/v8 expects react-router@^8. The examples differ in shape too. The v6 sample is the only one that builds a router for you, pairing createBrowserRouter and RouterProvider inside the adapter, while the v7 and v8 samples do nothing but wrap an Outlet inside the root route component.

For a reader this is a migration detail with teeth. Moving from React Router 6 to 7 changes the import path in every file that calls a hook, and v6 depends on react-router-dom while v7 and v8 depend on react-router. Nothing in the package aliases one for you, and there is no version range that makes a single subpath work across all three releases. If you pin React Router behind a compatibility layer, the adapter has to be pinned to the same layer, and the version notes are the only place that pairing is written down.

## Shallow mode is the default, so a server component can disagree with the address bar

The update path is deliberately shallow. The feature list describes shallow mode as the default for URL query updates, with opting in to notify server components. In a Next.js app that means the query string is rewritten and the client tree updates, but the server component payload for the route is not regenerated as a result of that write. Shallow is what keeps a text input responsive, and it is also why a param that a server component read can disagree with the param currently on screen until the next navigation or refresh.

The opt-in exists for the cases where agreement matters. If a page filters a server-rendered list from the same param a client control writes, you need that update to reach the server, and the cost is a server round trip per change. That cost is the reason the project also carries support for useTransition, so a server update can expose a loading state instead of appearing to stall. The consequence for the reader is that the mode is not a global preference you set once: every update site decides for itself, and getting it wrong produces a page that looks right until you reload it.

## The hooks are client-only, and server components read params through a separate cache

The usage example is explicit about where the hooks live. The file opens with 'use client', with the comment on that line reading: Only works in client components. It then calls useQueryState('name'), which returns a value and a setter, and the setter accepts either a plain string or null, with null being what the Clear button passes. The Clear button in the example is exactly that: onClick={() => setName(null)}.

That contract has a consequence for the server half of a page. Because the hook set is client-only, a server component cannot call it, so reads there go through the separate path the feature list calls a server cache, described as type-safe searchParams access in nested server components. The cache is the read side. The example contains no server-side setter, and the feature list does not claim one, so the pattern on a mixed tree is asymmetric: a client control writes, and a server component reads whatever it last rendered. If you need the server output to track the current param, shallow mode is the thing to revisit, because that is the setting controlling whether the write reaches the server at all.

## TanStack Router support is experimental and stops short of TanStack Start

TanStack Router is the one adapter the project hedges about. Its note reads: TanStack Router support is experimental and does not yet cover TanStack Start. The adapter is imported from nuqs/adapters/tanstack-router, the supported version is stated as @tanstack/react-router@^1, and the example attaches it inside the root route component built from createRootRoute, wrapping the Outlet.

The boundary is specific rather than vague. Client side TanStack Router projects are in scope, and anything running on TanStack Start is not. For a reader that means the adapter list in the README is longer than the set of frameworks you can deploy this on today, and the one entry carrying a warning is the one a Start project needs. The wider shape of the list matters too: every supported target is a React target. There is no Vue, Svelte or Angular entry, so a search that lands on nuqs for a non-React framework has nothing behind it, and the framework is chosen at install time by which adapter subpath your imports name.

## Replace or append decides whether the Back button replays your edits

History behaviour is a setting rather than a side effect. The feature list says you can replace history or append to use the Back button to navigate state updates, and the two are opposites. Replacing keeps a single entry per view, so Back leaves the page instead of stepping through each change. Appending pushes every update, which is what makes state updates navigable with the Back button, and is also what fills the history stack when a control writes on every keystroke.

There is a second hook for the multi-param case: useQueryStates, described as handling related querystrings. That is the one to reach for when several params must move together, since a related set is updated as a unit instead of one key at a time, and a half-applied filter is exactly the state you do not want to land on when a user hits Back.

What is missing is a stated default. Neither the feature list nor the README example says which mode applies when you do not pass one, so if Back button behaviour is part of your design, set the mode deliberately at each update site instead of assuming. Nothing here throttles writes either, so a control bound straight to setName will append one history entry per keystroke until you stop it.

## Parsers are the only place a custom URL shape can come from

Type safety is delivered by parsers, and the parser is also the only extension point for the URL itself. The built-in set covers common state types, with integer, float, boolean and Date named, and the documentation points you at creating your own parsers for custom types and pretty URLs. A parser therefore decides two things at once: the JavaScript value your component reads, and the text that ends up in the query string.

That coupling is the feature and the hazard. Change a parser and you change the URLs your users see, copy, bookmark and send, along with every link already circulating. The README example is the simplest case, useQueryState('name') with a raw string and a null reset, and no parser involved at all, which is why the pretty URL story is easy to miss when you start.

Two things the feature list leaves out are worth knowing before you build on this. There is no debounce anywhere in it, so writes arrive as fast as your input does. And nothing describes versioning the parameter format, so if you later rename a param or change its encoding there is no stated migration path for the links people already hold, and a rename reads to any consumer as a param that was never set.

## Conclusion

nuqs fits a React app whose filters, pagination and tabs belong in the address bar, provided you are on a supported adapter and can live with client-only hooks. It does not fit Vue or Svelte projects, it does not cover TanStack Start, and it will not notify server components unless you opt out of shallow mode on the update that matters. Before adopting it, confirm your framework has an adapter subpath at a version you already run, decide per update whether the Back button should replay that change, and check that any existing links you care about keep parsing under the params you intend to ship.

## FAQ

### What is nuqs?

A type-safe search params state manager for React frameworks, described as like useState, but stored in the URL query string. It ships adapters for the Next.js app and pages routers, plain React, Remix, React Router, TanStack Router, and custom routers.

### How do I install nuqs?

With one command, and the README covers six package managers: npm install nuqs, pnpm add nuqs, yarn add nuqs, bun add nuqs, deno add nuqs and vlt install nuqs.

### Does nuqs work with Vue?

No Vue entry point exists. Every adapter import in the README is a React one, from nuqs/adapters/react-router/v7 through nuqs/adapters/next/app, and no Vue or Svelte target is listed.

### Which Next.js versions does nuqs support?

The app router and pages router adapters require Next.js 14.2.0 or newer. For older versions, the note is to install nuqs@^1, which does not need this adapter code.

### Does nuqs work with TanStack Start?

Not yet. TanStack Router support is marked experimental and does not yet cover TanStack Start, and the adapter expects @tanstack/react-router@^1.

## Sources

- [47ng/nuqs on GitHub](https://github.com/47ng/nuqs)
- [License: MIT](https://github.com/47ng/nuqs/blob/next/LICENSE)
- [Project website](https://nuqs.dev)
- [README](https://github.com/47ng/nuqs/blob/next/README.md)
- [Releases](https://github.com/47ng/nuqs/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/47ng-nuqs
