Gathering detailed insights and metrics for nuqs
Gathering detailed insights and metrics for nuqs
Gathering detailed insights and metrics for nuqs
Gathering detailed insights and metrics for nuqs
Type-safe search params state manager for React frameworks - Like useState, but stored in the URL query string.
npm install nuqs
Typescript
Module System
Node Version
NPM Version
58
Supply Chain
81.5
Quality
94.6
Maintenance
50
Vulnerability
91.9
License
TypeScript (81.53%)
MDX (10.14%)
JavaScript (7.7%)
CSS (0.44%)
HTML (0.11%)
Shell (0.08%)
Total Downloads
7,319,802
Last Day
40,495
Last Week
326,371
Last Month
1,307,121
Last Year
7,319,718
5,418 Stars
889 Commits
124 Forks
10 Watching
8 Branches
29 Contributors
Latest Version
2.2.3
Package Id
nuqs@2.2.3
Unpacked Size
110.94 kB
Size
27.49 kB
File Count
40
NPM Version
10.9.0
Node Version
22.11.0
Publised On
23 Nov 2024
Cumulative downloads
Total Downloads
Last day
-30%
40,495
Compared to previous day
Last week
0.8%
326,371
Compared to previous week
Last month
35%
1,307,121
Compared to previous month
Last year
8,713,850%
7,319,718
Compared to previous year
1
4
21
Type-safe search params state manager for React frameworks. Like useState
, but stored in the URL query string.
app
and pages
routers), plain React (SPA), Remix, React Router, and custom routers via adaptersuseQueryStates
useTransition
to get loading states on server updatesRead the complete documentation at nuqs.47ng.com.
1pnpm add nuqs
1yarn add nuqs
1npm install nuqs
You will need to wrap your React component tree with an adapter for your framework. (expand the appropriate section below)
Supported Next.js versions:
>=14.2.0
. For older versions, installnuqs@^1
(which doesn't need this adapter code).
1// src/app/layout.tsx 2import { NuqsAdapter } from 'nuqs/adapters/next/app' 3import { type ReactNode } from 'react' 4 5export default function RootLayout({ children }: { children: ReactNode }) { 6 return ( 7 <html> 8 <body> 9 <NuqsAdapter>{children}</NuqsAdapter> 10 </body> 11 </html> 12 ) 13}
Supported Next.js versions:
>=14.2.0
. For older versions, installnuqs@^1
(which doesn't need this adapter code).
1// src/pages/_app.tsx 2import type { AppProps } from 'next/app' 3import { NuqsAdapter } from 'nuqs/adapters/next/pages' 4 5export default function MyApp({ Component, pageProps }: AppProps) { 6 return ( 7 <NuqsAdapter> 8 <Component {...pageProps} /> 9 </NuqsAdapter> 10 ) 11}
Example: via Vite or create-react-app.
1import { NuqsAdapter } from 'nuqs/adapters/react' 2 3createRoot(document.getElementById('root')!).render( 4 <NuqsAdapter> 5 <App /> 6 </NuqsAdapter> 7)
Supported Remix versions:
@remix-run/react@>=2
1// app/root.tsx 2import { NuqsAdapter } from 'nuqs/adapters/remix' 3 4// ... 5 6export default function App() { 7 return ( 8 <NuqsAdapter> 9 <Outlet /> 10 </NuqsAdapter> 11 ) 12}
Supported React Router versions:
react-router-dom@>=6
1import { NuqsAdapter } from 'nuqs/adapters/react-router' 2import { createBrowserRouter, RouterProvider } from 'react-router-dom' 3import App from './App' 4 5const router = createBrowserRouter([ 6 { 7 path: '/', 8 element: <App /> 9 } 10]) 11 12export function ReactRouter() { 13 return ( 14 <NuqsAdapter> 15 <RouterProvider router={router} /> 16 </NuqsAdapter> 17 ) 18}
1'use client' // Only works in client components 2 3import { useQueryState } from 'nuqs' 4 5export default () => { 6 const [name, setName] = useQueryState('name') 7 return ( 8 <> 9 <h1>Hello, {name || 'anonymous visitor'}!</h1> 10 <input value={name || ''} onChange={e => setName(e.target.value)} /> 11 <button onClick={() => setName(null)}>Clear</button> 12 </> 13 ) 14}
useQueryState
takes one required argument: the key to use in the query string.
Like React.useState
, it returns an array with the value present in the query
string as a string (or null
if none was found), and a state updater function.
Example outputs for our hello world example:
URL | name value | Notes |
---|---|---|
/ | null | No name key in URL |
/?name= | '' | Empty string |
/?name=foo | 'foo' | |
/?name=2 | '2' | Always returns a string by default, see Parsing below |
If your state type is not a string, you must pass a parsing function in the second argument object.
We provide parsers for common and more advanced object types:
1import { 2 parseAsString, 3 parseAsInteger, 4 parseAsFloat, 5 parseAsBoolean, 6 parseAsTimestamp, 7 parseAsIsoDateTime, 8 parseAsArrayOf, 9 parseAsJson, 10 parseAsStringEnum, 11 parseAsStringLiteral, 12 parseAsNumberLiteral 13} from 'nuqs' 14 15useQueryState('tag') // defaults to string 16useQueryState('count', parseAsInteger) 17useQueryState('brightness', parseAsFloat) 18useQueryState('darkMode', parseAsBoolean) 19useQueryState('after', parseAsTimestamp) // state is a Date 20useQueryState('date', parseAsIsoDateTime) // state is a Date 21useQueryState('array', parseAsArrayOf(parseAsInteger)) // state is number[] 22useQueryState('json', parseAsJson<Point>()) // state is a Point 23 24// Enums (string-based only) 25enum Direction { 26 up = 'UP', 27 down = 'DOWN', 28 left = 'LEFT', 29 right = 'RIGHT' 30} 31 32const [direction, setDirection] = useQueryState( 33 'direction', 34 parseAsStringEnum<Direction>(Object.values(Direction)) // pass a list of allowed values 35 .withDefault(Direction.up) 36) 37 38// Literals (string-based only) 39const colors = ['red', 'green', 'blue'] as const 40 41const [color, setColor] = useQueryState( 42 'color', 43 parseAsStringLiteral(colors) // pass a readonly list of allowed values 44 .withDefault('red') 45) 46 47// Literals (number-based only) 48const diceSides = [1, 2, 3, 4, 5, 6] as const 49 50const [side, setSide] = useQueryState( 51 'side', 52 parseAsNumberLiteral(diceSides) // pass a readonly list of allowed values 53 .withDefault(4) 54)
You may pass a custom set of parse
and serialize
functions:
1import { useQueryState } from 'nuqs'
2
3export default () => {
4 const [hex, setHex] = useQueryState('hex', {
5 // TypeScript will automatically infer it's a number
6 // based on what `parse` returns.
7 parse: (query: string) => parseInt(query, 16),
8 serialize: value => value.toString(16)
9 })
10}
Note: see the Accessing searchParams in server components section for a more user-friendly way to achieve type-safety.
If you wish to parse the searchParams in server components, you'll need to
import the parsers from nuqs/server
, which doesn't include
the "use client"
directive.
You can then use the parseServerSide
method:
1import { parseAsInteger } from 'nuqs/server' 2 3type PageProps = { 4 searchParams: { 5 counter?: string | string[] 6 } 7} 8 9const counterParser = parseAsInteger.withDefault(1) 10 11export default function ServerPage({ searchParams }: PageProps) { 12 const counter = counterParser.parseServerSide(searchParams.counter) 13 console.log('Server side counter: %d', counter) 14 return ( 15 ... 16 ) 17}
See the server-side parsing demo for a live example showing how to reuse parser configurations between client and server code.
Note: parsers don't validate your data. If you expect positive integers or JSON-encoded objects of a particular shape, you'll need to feed the result of the parser to a schema validation library, like Zod.
When the query string is not present in the URL, the default behaviour is to
return null
as state.
It can make state updating and UI rendering tedious. Take this example of a simple counter stored in the URL:
1import { useQueryState, parseAsInteger } from 'nuqs' 2 3export default () => { 4 const [count, setCount] = useQueryState('count', parseAsInteger) 5 return ( 6 <> 7 <pre>count: {count}</pre> 8 <button onClick={() => setCount(0)}>Reset</button> 9 {/* handling null values in setCount is annoying: */} 10 <button onClick={() => setCount(c => c ?? 0 + 1)}>+</button> 11 <button onClick={() => setCount(c => c ?? 0 - 1)}>-</button> 12 <button onClick={() => setCount(null)}>Clear</button> 13 </> 14 ) 15}
You can specify a default value to be returned in this case:
1const [count, setCount] = useQueryState('count', parseAsInteger.withDefault(0)) 2 3const increment = () => setCount(c => c + 1) // c will never be null 4const decrement = () => setCount(c => c - 1) // c will never be null 5const clearCount = () => setCount(null) // Remove query from the URL
Note: the default value is internal to React, it will not be written to the URL.
Setting the state to null
will remove the key in the query string and set the
state to the default value.
By default, state updates are done by replacing the current history entry with the updated query when state changes.
You can see this as a sort of git squash
, where all state-changing
operations are merged into a single history value.
You can also opt-in to push a new history item for each state change, per key, which will let you use the Back button to navigate state updates:
1// Default: replace current history with new state 2useQueryState('foo', { history: 'replace' }) 3 4// Append state changes to history: 5useQueryState('foo', { history: 'push' })
Any other value for the history
option will fallback to the default.
You can also override the history mode when calling the state updater function:
1const [query, setQuery] = useQueryState('q', { history: 'push' }) 2 3// This overrides the hook declaration setting: 4setQuery(null, { history: 'replace' })
Note: this feature only applies to Next.js
By default, query state updates are done in a client-first manner: there are no network calls to the server.
This is equivalent to the shallow
option of the Next.js pages router set to true
,
or going through the experimental windowHistorySupport
flag in the app router.
To opt-in to query updates notifying the server (to re-run getServerSideProps
in the pages router and re-render Server Components on the app router),
you can set shallow
to false
:
1const [state, setState] = useQueryState('foo', { shallow: false }) 2 3// You can also pass the option on calls to setState: 4setState('bar', { shallow: false })
Because of browsers rate-limiting the History API, internal updates to the URL are queued and throttled to a default of 50ms, which seems to satisfy most browsers even when sending high-frequency query updates, like binding to a text input or a slider.
Safari's rate limits are much higher and would require a throttle of around 340ms. If you end up needing a longer time between updates, you can specify it in the options:
1useQueryState('foo', {
2 // Send updates to the server maximum once every second
3 shallow: false,
4 throttleMs: 1000
5})
6
7// You can also pass the option on calls to setState:
8setState('bar', { throttleMs: 1000 })
Note: the state returned by the hook is always updated instantly, to keep UI responsive. Only changes to the URL, and server requests when using
shallow: false
, are throttled.
If multiple hooks set different throttle values on the same event loop tick, the highest value will be used. Also, values lower than 50ms will be ignored, to avoid rate-limiting issues. Read more.
When combined with shallow: false
, you can use the useTransition
hook to get
loading states while the server is re-rendering server components with the
updated URL.
Pass in the startTransition
function from useTransition
to the options
to enable this behaviour:
1'use client'
2
3import React from 'react'
4import { useQueryState, parseAsString } from 'nuqs'
5
6function ClientComponent({ data }) {
7 // 1. Provide your own useTransition hook:
8 const [isLoading, startTransition] = React.useTransition()
9 const [query, setQuery] = useQueryState(
10 'query',
11 // 2. Pass the `startTransition` as an option:
12 parseAsString().withOptions({
13 startTransition,
14 shallow: false // opt-in to notify the server (Next.js only)
15 })
16 )
17 // 3. `isLoading` will be true while the server is re-rendering
18 // and streaming RSC payloads, when the query is updated via `setQuery`.
19
20 // Indicate loading state
21 if (isLoading) return <div>Loading...</div>
22
23 // Normal rendering with data
24 return <div>{/*...*/}</div>
25}
You can use a builder pattern to facilitate specifying all of those things:
1useQueryState(
2 'counter',
3 parseAsInteger.withDefault(0).withOptions({
4 history: 'push',
5 shallow: false
6 })
7)
You can get this pattern for your custom parsers too, and compose them with others:
1import { createParser, parseAsHex } from 'nuqs' 2 3// Wrapping your parser/serializer in `createParser` 4// gives it access to the builder pattern & server-side 5// parsing capabilities: 6const hexColorSchema = createParser({ 7 parse(query) { 8 if (query.length !== 6) { 9 return null // always return null for invalid inputs 10 } 11 return { 12 // When composing other parsers, they may return null too. 13 r: parseAsHex.parse(query.slice(0, 2)) ?? 0x00, 14 g: parseAsHex.parse(query.slice(2, 4)) ?? 0x00, 15 b: parseAsHex.parse(query.slice(4)) ?? 0x00 16 } 17 }, 18 serialize({ r, g, b }) { 19 return ( 20 parseAsHex.serialize(r) + 21 parseAsHex.serialize(g) + 22 parseAsHex.serialize(b) 23 ) 24 } 25}) 26 // Eg: set common options directly 27 .withOptions({ history: 'push' }) 28 29// Or on usage: 30useQueryState( 31 'tribute', 32 hexColorSchema.withDefault({ 33 r: 0x66, 34 g: 0x33, 35 b: 0x99 36 }) 37)
Note: see this example running in the hex-colors demo.
You can call as many state update function as needed in a single event loop tick, and they will be applied to the URL asynchronously:
1const MultipleQueriesDemo = () => { 2 const [lat, setLat] = useQueryState('lat', parseAsFloat) 3 const [lng, setLng] = useQueryState('lng', parseAsFloat) 4 const randomCoordinates = React.useCallback(() => { 5 setLat(Math.random() * 180 - 90) 6 setLng(Math.random() * 360 - 180) 7 }, []) 8}
If you wish to know when the URL has been updated, and what it contains, you can await the Promise returned by the state updater function, which gives you the updated URLSearchParameters object:
1const randomCoordinates = React.useCallback(() => { 2 setLat(42) 3 return setLng(12) 4}, []) 5 6randomCoordinates().then((search: URLSearchParams) => { 7 search.get('lat') // 42 8 search.get('lng') // 12, has been queued and batch-updated 9})
The returned Promise is cached until the next flush to the URL occurs, so all calls to a setState (of any hook) in the same event loop tick will return the same Promise reference.
Due to throttling of calls to the Web History API, the Promise may be cached for several ticks. Batched updates will be merged and flushed once to the URL. This means not every setState will reflect to the URL, if another one comes overriding it before flush occurs.
The returned React state will reflect all set values instantly, to keep UI responsive.
useQueryStates
For query keys that should always move together, you can use useQueryStates
with an object containing each key's type:
1import { useQueryStates, parseAsFloat } from 'nuqs' 2 3const [coordinates, setCoordinates] = useQueryStates( 4 { 5 lat: parseAsFloat.withDefault(45.18), 6 lng: parseAsFloat.withDefault(5.72) 7 }, 8 { 9 history: 'push' 10 } 11) 12 13const { lat, lng } = coordinates 14 15// Set all (or a subset of) the keys in one go: 16const search = await setCoordinates({ 17 lat: Math.random() * 180 - 90, 18 lng: Math.random() * 360 - 180 19})
If you wish to access the searchParams in a deeply nested Server Component
(ie: not in the Page component), you can use createSearchParamsCache
to do so in a type-safe manner.
Note: parsers don't validate your data. If you expect positive integers or JSON-encoded objects of a particular shape, you'll need to feed the result of the parser to a schema validation library, like Zod.
1// searchParams.ts 2import { 3 createSearchParamsCache, 4 parseAsInteger, 5 parseAsString 6} from 'nuqs/server' 7// Note: import from 'nuqs/server' to avoid the "use client" directive 8 9export const searchParamsCache = createSearchParamsCache({ 10 // List your search param keys and associated parsers here: 11 q: parseAsString.withDefault(''), 12 maxResults: parseAsInteger.withDefault(10) 13}) 14 15// page.tsx 16import { searchParamsCache } from './searchParams' 17 18export default function Page({ 19 searchParams 20}: { 21 searchParams: Record<string, string | string[] | undefined> 22}) { 23 // ⚠️ Don't forget to call `parse` here. 24 // You can access type-safe values from the returned object: 25 const { q: query } = searchParamsCache.parse(searchParams) 26 return ( 27 <div> 28 <h1>Search Results for {query}</h1> 29 <Results /> 30 </div> 31 ) 32} 33 34function Results() { 35 // Access type-safe search params in children server components: 36 const maxResults = searchParamsCache.get('maxResults') 37 return <span>Showing up to {maxResults} results</span> 38}
The cache will only be valid for the current page render
(see React's cache
function).
Note: the cache only works for server components, but you may share your
parser declaration with useQueryStates
for type-safety in client components:
1// searchParams.ts 2import { parseAsFloat, createSearchParamsCache } from 'nuqs/server' 3 4export const coordinatesParsers = { 5 lat: parseAsFloat.withDefault(45.18), 6 lng: parseAsFloat.withDefault(5.72) 7} 8export const coordinatesCache = createSearchParamsCache(coordinatesParsers) 9 10// page.tsx 11import { coordinatesCache } from './searchParams' 12import { Server } from './server' 13import { Client } from './client' 14 15export default async function Page({ searchParams }) { 16 await coordinatesCache.parse(searchParams) 17 return ( 18 <> 19 <Server /> 20 <Suspense> 21 <Client /> 22 </Suspense> 23 </> 24 ) 25} 26 27// server.tsx 28import { coordinatesCache } from './searchParams' 29 30export function Server() { 31 const { lat, lng } = coordinatesCache.all() 32 // or access keys individually: 33 const lat = coordinatesCache.get('lat') 34 const lng = coordinatesCache.get('lng') 35 return ( 36 <span> 37 Latitude: {lat} - Longitude: {lng} 38 </span> 39 ) 40} 41 42// client.tsx 43// prettier-ignore 44;'use client' 45 46import { useQueryStates } from 'nuqs' 47import { coordinatesParsers } from './searchParams' 48 49export function Client() { 50 const [{ lat, lng }, setCoordinates] = useQueryStates(coordinatesParsers) 51 // ... 52}
To populate <Link>
components with state values, you can use the createSerializer
helper.
Pass it an object describing your search params, and it will give you a function to call with values, that generates a query string serialized as the hooks would do.
Example:
1import { 2 createSerializer, 3 parseAsInteger, 4 parseAsIsoDateTime, 5 parseAsString, 6 parseAsStringLiteral 7} from 'nuqs/server' 8 9const searchParams = { 10 search: parseAsString, 11 limit: parseAsInteger, 12 from: parseAsIsoDateTime, 13 to: parseAsIsoDateTime, 14 sortBy: parseAsStringLiteral(['asc', 'desc'] as const) 15} 16 17// Create a serializer function by passing the description of the search params to accept 18const serialize = createSerializer(searchParams) 19 20// Then later, pass it some values (a subset) and render them to a query string 21serialize({ 22 search: 'foo bar', 23 limit: 10, 24 from: new Date('2024-01-01'), 25 // here, we omit `to`, which won't be added 26 sortBy: null // null values are also not rendered 27}) 28// ?search=foo+bar&limit=10&from=2024-01-01T00:00:00.000Z
The returned serialize
function can take a base parameter over which to
append/amend the search params:
1serialize('/path?baz=qux', { foo: 'bar' }) // /path?baz=qux&foo=bar 2 3const search = new URLSearchParams('?baz=qux') 4serialize(search, { foo: 'bar' }) // ?baz=qux&foo=bar 5 6const url = new URL('https://example.com/path?baz=qux') 7serialize(url, { foo: 'bar' }) // https://example.com/path?baz=qux&foo=bar 8 9// Passing null removes existing values 10serialize('?remove=me', { foo: 'bar', remove: null }) // ?foo=bar
To access the underlying type returned by a parser, you can use the
inferParserType
type helper:
1import { parseAsInteger, type inferParserType } from 'nuqs' // or 'nuqs/server' 2 3const intNullable = parseAsInteger 4const intNonNull = parseAsInteger.withDefault(0) 5 6inferParserType<typeof intNullable> // number | null 7inferParserType<typeof intNonNull> // number
For an object describing parsers (that you'd pass to createSearchParamsCache
or to useQueryStates
, inferParserType
will
return the type of the object with the parsers replaced by their inferred types:
1import { parseAsBoolean, parseAsInteger, type inferParserType } from 'nuqs' // or 'nuqs/server' 2 3const parsers = { 4 a: parseAsInteger, 5 b: parseAsBoolean.withDefault(false) 6} 7 8inferParserType<typeof parsers> 9// { a: number | null, b: boolean }
Since nuqs v2, you can use a testing adapter to unit-test components using
useQueryState
and useQueryStates
in isolation, without needing to mock
your framework or router.
Here's an example using Testing Library and Vitest:
1import { render, screen } from '@testing-library/react' 2import userEvent from '@testing-library/user-event' 3import { NuqsTestingAdapter, type UrlUpdateEvent } from 'nuqs/adapters/testing' 4import { describe, expect, it, vi } from 'vitest' 5import { CounterButton } from './counter-button' 6 7it('should increment the count when clicked', async () => { 8 const user = userEvent.setup() 9 const onUrlUpdate = vi.fn<[UrlUpdateEvent]>() 10 render(<CounterButton />, { 11 // Setup the test by passing initial search params / querystring, 12 // and give it a function to call on URL updates 13 wrapper: ({ children }) => ( 14 <NuqsTestingAdapter searchParams="?count=42" onUrlUpdate={onUrlUpdate}> 15 {children} 16 </NuqsTestingAdapter> 17 ) 18 }) 19 // Initial state assertions: there's a clickable button displaying the count 20 const button = screen.getByRole('button') 21 expect(button).toHaveTextContent('count is 42') 22 // Act 23 await user.click(button) 24 // Assert changes in the state and in the (mocked) URL 25 expect(button).toHaveTextContent('count is 43') 26 expect(onUrlUpdate).toHaveBeenCalledOnce() 27 expect(onUrlUpdate.mock.calls[0][0].queryString).toBe('?count=43') 28 expect(onUrlUpdate.mock.calls[0][0].searchParams.get('count')).toBe('43') 29 expect(onUrlUpdate.mock.calls[0][0].options.history).toBe('push') 30})
See #259 for more testing-related discussions.
You can enable debug logs in the browser by setting the debug
item in localStorage
to nuqs
, and reload the page.
1// In your devtools: 2localStorage.setItem('debug', 'nuqs')
Note: unlike the
debug
package, this will not work with wildcards, but you can combine it:localStorage.setItem('debug', '*,nuqs')
Log lines will be prefixed with [nuqs]
for useQueryState
and [nuq+]
for
useQueryStates
, along with other internal debug logs.
User timings markers are also recorded, for advanced performance analysis using your browser's devtools.
Providing debug logs when opening an issue is always appreciated. 🙏
If your page uses query strings for local-only state, you should add a canonical URL to your page, to tell SEO crawlers to ignore the query string and index the page without it.
In the app router, this is done via the metadata object:
1import type { Metadata } from 'next' 2 3export const metadata: Metadata = { 4 alternates: { 5 canonical: '/url/path/without/querystring' 6 } 7}
If however the query string is defining what content the page is displaying
(eg: YouTube's watch URLs, like https://www.youtube.com/watch?v=dQw4w9WgXcQ
),
your canonical URL should contain relevant query strings, and you can still
use useQueryState
to read it:
1// page.tsx 2import type { Metadata, ResolvingMetadata } from 'next' 3import { useQueryState } from 'nuqs' 4import { parseAsString } from 'nuqs/server' 5 6type Props = { 7 searchParams: { [key: string]: string | string[] | undefined } 8} 9 10export async function generateMetadata({ 11 searchParams 12}: Props): Promise<Metadata> { 13 const videoId = parseAsString.parseServerSide(searchParams.v) 14 return { 15 alternates: { 16 canonical: `/watch?v=${videoId}` 17 } 18 } 19}
If your serializer loses precision or doesn't accurately represent the underlying state value, you will lose this precision when reloading the page or restoring state from the URL (eg: on navigation).
Example:
1const geoCoordParser = { 2 parse: parseFloat, 3 serialize: v => v.toFixed(4) // Loses precision 4} 5 6const [lat, setLat] = useQueryState('lat', geoCoordParser)
Here, setting a latitude of 1.23456789 will render a URL query string
of lat=1.2345
, while the internal lat
state will be correctly
set to 1.23456789.
Upon reloading the page, the state will be incorrectly set to 1.2345.
Made with ❤️ by François Best
Using this package at work ? Sponsor me to help with support and maintenance.
No vulnerabilities found.
No security vulnerabilities found.