| name | nuqs |
| description | Use when implementing URL query state in React, managing search params, syncing state with URL, building filterable/sortable lists, pagination with URL state, or using nuqs/useQueryState/useQueryStates hooks in Next.js, Remix, React Router, or plain React. |
nuqs Best Practices
Type-safe URL query state management for React. Like useState, but stored in the URL.
Setup (Required First)
Wrap your app with the appropriate adapter:
import { NuqsAdapter } from 'nuqs/adapters/next/app'
export default function RootLayout({ children }) {
return <NuqsAdapter>{children}</NuqsAdapter>
}
import { NuqsAdapter } from 'nuqs/adapters/next/pages'
import { NuqsAdapter } from 'nuqs/adapters/react'
import { NuqsAdapter } from 'nuqs/adapters/remix'
import { NuqsAdapter } from 'nuqs/adapters/react-router'
Global Options
import { throttle } from 'nuqs'
<NuqsAdapter
defaultOptions={{
shallow: false,
scroll: true,
clearOnDefault: true,
limitUrlUpdates: throttle(250)
}}
>
{children}
</NuqsAdapter>
Core API
Single Parameter
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
const [search, setSearch] = useQueryState('q')
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
setSearch('hello')
setSearch(null)
setPage(p => p + 1)
await setPage(5)
Multiple Parameters
import { useQueryStates, parseAsInteger, parseAsString } from 'nuqs'
const [filters, setFilters] = useQueryStates({
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1),
sort: parseAsString.withDefault('date')
})
setFilters({ page: 1, sort: 'name' })
const params = await setFilters({ page: 2 })
params.get('page')
Built-in Parsers
| Parser | Type | Example URL |
|---|
parseAsString | string | ?q=hello |
parseAsInteger | number | ?page=1 |
parseAsFloat | number | ?price=9.99 |
parseAsHex | number | ?color=ff0000 |
parseAsBoolean | boolean | ?active=true |
parseAsIsoDateTime | Date | ?date=2024-01-15T10:30:00Z |
parseAsTimestamp | Date | ?t=1705312200000 |
parseAsArrayOf(parser) | T[] | ?tags=a,b,c |
parseAsArrayOf(parser, ';') | T[] | ?ids=1;2;3 (custom separator) |
parseAsJson<T>() | T | ?data={"key":"value"} |
parseAsStringEnum(values) | enum | ?status=active |
parseAsStringLiteral(arr) | literal | ?sort=asc |
parseAsNumberLiteral(arr) | literal | ?dice=6 |
Enum & Literal Examples
enum Status { Active = 'active', Inactive = 'inactive' }
const [status] = useQueryState('status',
parseAsStringEnum(Object.values(Status)).withDefault(Status.Active)
)
const sortOptions = ['asc', 'desc'] as const
const [sort] = useQueryState('sort',
parseAsStringLiteral(sortOptions).withDefault('asc')
)
const diceSides = [1, 2, 3, 4, 5, 6] as const
const [dice] = useQueryState('dice',
parseAsNumberLiteral(diceSides).withDefault(1)
)
Arrays
const [tags, setTags] = useQueryState('tags',
parseAsArrayOf(parseAsString).withDefault([])
)
const [ids] = useQueryState('ids',
parseAsArrayOf(parseAsInteger, ';').withDefault([])
)
Options
useQueryState('key', parseAsString.withOptions({
history: 'push',
shallow: false,
scroll: false,
throttleMs: 500,
clearOnDefault: true,
startTransition,
}))
Options precedence: call-level > parser-level > hook-level > global adapter
const parser = parseAsString.withOptions({ shallow: false })
const [q, setQ] = useQueryState('q', parser, { history: 'push' })
setQ('value', { shallow: true })
Functional Updates & Batching
setCount(c => c + 1)
setCount(c => c * 2)
function onClick() {
setCount(x => x + 1)
setCount(x => x * 2)
}
const search = await setFilters({ page: 2 })
search.get('page')
Loading States with useTransition
'use client'
import { useTransition } from 'react'
import { useQueryState, parseAsString } from 'nuqs'
function Search({ results }) {
const [isLoading, startTransition] = useTransition()
const [query, setQuery] = useQueryState('q',
parseAsString.withOptions({
startTransition,
shallow: false
})
)
return (
<>
<input value={query ?? ''} onChange={e => setQuery(e.target.value)} />
{isLoading ? <Spinner /> : <Results data={results} />}
</>
)
}
Custom Parsers
Basic Custom Parser
const parseAsDate = {
parse: (value: string) => new Date(value),
serialize: (date: Date) => date.toISOString().split('T')[0]
}
const [date, setDate] = useQueryState('date', parseAsDate)
With createParser (for reference types)
For non-primitive types, provide eq function for clearOnDefault to work:
import { createParser, parseAsStringLiteral } from 'nuqs'
const parseAsDate = createParser({
parse: (value: string) => new Date(value.slice(0, 10)),
serialize: (date: Date) => date.toISOString().slice(0, 10),
eq: (a: Date, b: Date) => a.getTime() === b.getTime()
})
const parseAsSort = createParser({
parse(query) {
const [id = '', dir = ''] = query.split(':')
return { id, desc: dir === 'desc' }
},
serialize(value) {
return `${value.id}:`
},
() {
a. === b. && a. === b.
}
})
Server Components (Next.js)
import { createSearchParamsCache, parseAsInteger, parseAsString } from 'nuqs/server'
export const searchParamsCache = createSearchParamsCache({
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1)
})
import { searchParamsCache } from '@/lib/searchParams'
import type { SearchParams } from 'nuqs/server'
type Props = { searchParams: Promise<SearchParams> }
export default async function Page({ searchParams }: Props) {
const { q, page } = await searchParamsCache.parse(searchParams)
return <Results query={q} page={page} />
}
function NestedComponent() {
const page = searchParamsCache.()
}
Reusable Patterns
Shared Parser Definitions
export const paginationParsers = {
page: parseAsInteger.withDefault(1),
limit: parseAsInteger.withDefault(20),
sort: parseAsString.withDefault('createdAt'),
order: parseAsStringLiteral(['asc', 'desc'] as const).withDefault('desc')
}
const [pagination, setPagination] = useQueryStates(paginationParsers)
URL Key Mapping
const [coords, setCoords] = useQueryStates(
{
latitude: parseAsFloat.withDefault(0),
longitude: parseAsFloat.withDefault(0)
},
{
urlKeys: { latitude: 'lat', longitude: 'lng' }
}
)
Custom Hook
export function useFilters() {
return useQueryStates({
search: parseAsString.withDefault(''),
category: parseAsString,
minPrice: parseAsFloat,
maxPrice: parseAsFloat,
inStock: parseAsBoolean.withDefault(false)
})
}
const [filters, setFilters] = useFilters()
Testing
import { withNuqsTestingAdapter, type UrlUpdateEvent } from 'nuqs/adapters/testing'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
it('updates URL on click', async () => {
const user = userEvent.setup()
const onUrlUpdate = vi.fn<[UrlUpdateEvent]>()
render(<CounterButton />, {
wrapper: withNuqsTestingAdapter({
searchParams: '?count=1',
onUrlUpdate
})
})
await user.click(screen.getByRole('button'))
expect(screen.getByRole('button')).toHaveTextContent('count is 2')
expect(onUrlUpdate).toHaveBeenCalledOnce()
const event = onUrlUpdate.mock.calls[0]![0]!
expect(event.queryString).toBe('?count=2')
expect(event.searchParams.()).()
(event..).()
})
Critical Mistakes to Avoid
1. Missing Adapter
useQueryState('q')
2. Wrong Adapter for Framework
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import { NuqsAdapter } from 'nuqs/adapters/next/pages'
3. Missing Suspense (Next.js App Router)
export default function Page() {
const [q] = useQueryState('q')
return <div>{q}</div>
}
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<SearchClient />
</Suspense>
)
}
4. Same Key, Different Parsers
const [intVal] = useQueryState('foo', parseAsInteger)
const [floatVal] = useQueryState('foo', parseAsFloat)
function useFoo() {
const [val, setVal] = useQueryState('foo', parseAsFloat)
return { float: val, int: Math.floor(val ?? 0), setVal }
}
5. Forgetting to Parse on Server
const values = searchParamsCache
const values = await searchParamsCache.parse(searchParams)
6. Server Component with Client Hook
export default function Page() {
const [q] = useQueryState('q')
}
7. Not Handling Null Without Default
const [count, setCount] = useQueryState('count', parseAsInteger)
setCount(c => (c ?? 0) + 1)
const [count, setCount] = useQueryState('count', parseAsInteger.withDefault(0))
setCount(c => c + 1)
8. Lossy Serialization
const geoParser = {
parse: parseFloat,
serialize: v => v.toFixed(2)
}
const geoParser = {
parse: parseFloat,
serialize: v => v.toString()
}
9. Missing eq for Reference Types
const dateParser = {
parse: (v) => new Date(v),
serialize: (d) => d.toISOString()
}
const dateParser = createParser({
parse: (v) => new Date(v),
serialize: (d) => d.toISOString(),
eq: (a, b) => a.getTime() === b.getTime()
})
Quick Reference
| Task | Solution |
|---|
| Single param | useQueryState('key', parser.withDefault(val)) |
| Multiple params | useQueryStates({ key: parser }) |
| Server access | createSearchParamsCache + .parse() |
| Notify server | { shallow: false } |
| History entry | { history: 'push' } |
| Loading state | useTransition + { startTransition } |
| Short URL keys | urlKeys: { longName: 'short' } |
| Array param | parseAsArrayOf(parser) or parseAsArrayOf(parser, ';') |
| Enum/literal | parseAsStringLiteral(['a', 'b'] as const) |
| Custom type | createParser({ parse, serialize, eq }) |
| Test component | withNuqsTestingAdapter({ searchParams: '?...' }) |