nuqs
π―Skillfrom noklip-io/agent-skills
Manages URL query state in React with type-safe hooks, enabling seamless state synchronization and URL-based interactions like search, pagination, and filtering.
Part of
noklip-io/agent-skills(7 items)
Installation
npx skills add noklip-io/agent-skillsnpx skills add noklip-io/agent-skills --skill three-jsnpx skills add noklip-io/agent-skills --skill react-19npx skills add noklip-io/agent-skills --skill nuqsnpx skills add noklip-io/agent-skills --skill theatre-js+ 4 more commands
Skill Details
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.
Overview
# 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:
```tsx
// Next.js App Router - app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app'
export default function RootLayout({ children }) {
return
}
// Next.js Pages Router - pages/_app.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/pages'
// React SPA (Vite/CRA)
import { NuqsAdapter } from 'nuqs/adapters/react'
// Remix - app/root.tsx
import { NuqsAdapter } from 'nuqs/adapters/remix'
// React Router v6/v7
import { NuqsAdapter } from 'nuqs/adapters/react-router'
```
Global Options
```tsx
import { throttle } from 'nuqs'
defaultOptions={{
shallow: false, // notify server by default
scroll: true, // scroll to top on change
clearOnDefault: true, // remove param when equals default
limitUrlUpdates: throttle(250) // throttle URL updates
}}
>
{children}
```
Core API
Single Parameter
```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
// String (default) - returns null | string
const [search, setSearch] = useQueryState('q')
// With parser + default (recommended)
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
// Updates
setSearch('hello') // ?q=hello
setSearch(null) // removes param
setPage(p => p + 1) // functional update
await setPage(5) // returns Promise
```
Multiple Parameters
```tsx
import { useQueryStates, parseAsInteger, parseAsString } from 'nuqs'
const [filters, setFilters] = useQueryStates({
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1),
sort: parseAsString.withDefault('date')
})
// Partial updates
setFilters({ page: 1, sort: 'name' })
// Await batch update
const params = await setFilters({ page: 2 })
params.get('page') // '2'
```
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 | ?data={"key":"value"} |
| parseAsStringEnum(values) | enum | ?status=active |
| parseAsStringLiteral(arr) | literal | ?sort=asc |
| parseAsNumberLiteral(arr) | literal | ?dice=6 |
Enum & Literal Examples
```tsx
// String enum
enum Status { Active = 'active', Inactive = 'inactive' }
const [status] = useQueryState('status',
parseAsStringEnum(Object.values(Status)).withDefault(Status.Active)
)
// String literal (type-safe)
const sortOptions = ['asc', 'desc'] as const
const [sort] = useQueryState('sort',
parseAsStringLiteral(sortOptions).withDefault('asc')
)
// Number literal
const diceSides = [1, 2, 3, 4, 5, 6] as const
const [dice] = useQueryState('dice',
parseAsNumberLiteral(diceSides).withDefault(1)
)
```
Arrays
```tsx
// Default comma separator: ?tags=react,typescript,nuqs
const [tags, setTags] = useQueryState('tags',
parseAsArrayOf(parseAsString).withDefault([])
)
// Custom separator: ?ids=1;2;3
const [ids] = useQueryState('ids',
parseAsArrayOf(parseAsInteger, ';').withDefault([])
)
```
Options
```tsx
useQueryState('key', parseAsString.withOptions({
history: 'push', // 'push' | 'replace' (default)
shallow: false, // true (default) = client only, false = notify server
scroll: false, // scroll to top on change
throttleMs: 500, // throttle URL updates (min 50ms)
clearOnDefault: true, // remove param when equals default (default: true)
startTransition, // React useTransition for loading states
}))
```
Options precedence: call-level > parser-level > hook-level > global adapter
```tsx
// Parser-level options
const parser = parseAsString.withOptions({ shallow: false })
// Hook-level options
const [q, setQ] = useQueryState('q', parser, { history: 'push' })
// Call-level override (highest priority)
setQ('value', { shallow: true })
```
Functional Updates & Batching
```tsx
// Functional updates
setCount(c => c + 1)
setCount(c => c * 2) // Both batched in same tick
// Chained functional updates execute in order
function onClick() {
setCount(x => x + 1) // 0 β 1
setCount(x => x * 2) // 1 β 2
}
// Await updates
const search = await setFilters({ page: 2 })
search.get('page') // '2'
```
Loading States with useTransition
```tsx
'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, // enables loading state
shallow: false // required for server updates
})
)
return (
<>
setQuery(e.target.value)} />
{isLoading ?
>
)
}
```
Custom Parsers
Basic Custom Parser
```tsx
// Simple date 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:
```tsx
import { createParser, parseAsStringLiteral } from 'nuqs'
// Date with equality check
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()
})
// Complex type (e.g., TanStack Table sort state)
// URL: ?sort=name:asc β { id: 'name', desc: false }
const parseAsSort = createParser({
parse(query) {
const [id = '', dir = ''] = query.split(':')
return { id, desc: dir === 'desc' }
},
serialize(value) {
return ${value.id}:${value.desc ? 'desc' : 'asc'}
},
eq(a, b) {
return a.id === b.id && a.desc === b.desc
}
})
```
Server Components (Next.js)
```tsx
// lib/searchParams.ts
import { createSearchParamsCache, parseAsInteger, parseAsString } from 'nuqs/server'
export const searchParamsCache = createSearchParamsCache({
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1)
})
// app/search/page.tsx (Server Component)
import { searchParamsCache } from '@/lib/searchParams'
import type { SearchParams } from 'nuqs/server'
type Props = { searchParams: Promise
export default async function Page({ searchParams }: Props) {
// β οΈ Must call parse() - don't forget!
const { q, page } = await searchParamsCache.parse(searchParams)
return
}
// Nested server component - no props needed
function NestedComponent() {
const page = searchParamsCache.get('page') // type-safe!
return Page {page}
}
```
Reusable Patterns
Shared Parser Definitions
```tsx
// lib/parsers.ts
export const paginationParsers = {
page: parseAsInteger.withDefault(1),
limit: parseAsInteger.withDefault(20),
sort: parseAsString.withDefault('createdAt'),
order: parseAsStringLiteral(['asc', 'desc'] as const).withDefault('desc')
}
// Component
const [pagination, setPagination] = useQueryStates(paginationParsers)
```
URL Key Mapping
```tsx
const [coords, setCoords] = useQueryStates(
{
latitude: parseAsFloat.withDefault(0),
longitude: parseAsFloat.withDefault(0)
},
{
urlKeys: { latitude: 'lat', longitude: 'lng' }
}
)
// URL: ?lat=45.5&lng=-122.6
// Code: coords.latitude, coords.longitude
```
Custom Hook
```tsx
// hooks/useFilters.ts
export function useFilters() {
return useQueryStates({
search: parseAsString.withDefault(''),
category: parseAsString,
minPrice: parseAsFloat,
maxPrice: parseAsFloat,
inStock: parseAsBoolean.withDefault(false)
})
}
// Component
const [filters, setFilters] = useFilters()
```
Testing
```tsx
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(
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.get('count')).toBe('2')
expect(event.options.history).toBe('push')
})
```
Critical Mistakes to Avoid
1. Missing Adapter
```tsx
// β Error: nuqs requires an adapter
useQueryState('q')
// β Wrap app in NuqsAdapter first (see Setup section)
```
2. Wrong Adapter for Framework
```tsx
// β Using app router adapter in pages router
import { NuqsAdapter } from 'nuqs/adapters/next/app' // Wrong!
// β Match adapter to your router
import { NuqsAdapter } from 'nuqs/adapters/next/pages'
```
3. Missing Suspense (Next.js App Router)
```tsx
// β Hydration error
export default function Page() {
const [q] = useQueryState('q')
return
}
// β Wrap client components in Suspense
export default function Page() {
return (
)
}
```
4. Same Key, Different Parsers
```tsx
// β Conflicts - last update wins with wrong type
const [intVal] = useQueryState('foo', parseAsInteger)
const [floatVal] = useQueryState('foo', parseAsFloat)
// β One parser per key, share via custom hook
function useFoo() {
const [val, setVal] = useQueryState('foo', parseAsFloat)
return { float: val, int: Math.floor(val ?? 0), setVal }
}
```
5. Forgetting to Parse on Server
```tsx
// β Returns cache object, not values
const values = searchParamsCache // Wrong!
// β Call parse() with searchParams prop
const values = await searchParamsCache.parse(searchParams)
```
6. Server Component with Client Hook
```tsx
// β useQueryState only works in client components
export default function Page() { // Server component
const [q] = useQueryState('q') // Error!
}
// β Use createSearchParamsCache for server, useQueryState for client
```
7. Not Handling Null Without Default
```tsx
// β Tedious null handling
const [count, setCount] = useQueryState('count', parseAsInteger)
setCount(c => (c ?? 0) + 1) // Must handle null every time
// β Use withDefault
const [count, setCount] = useQueryState('count', parseAsInteger.withDefault(0))
setCount(c => c + 1) // Always a number
```
8. Lossy Serialization
```tsx
// β Loses precision on reload
const geoParser = {
parse: parseFloat,
serialize: v => v.toFixed(2) // 1.23456 β "1.23" β 1.23
}
// β Preserve precision or accept the tradeoff knowingly
const geoParser = {
parse: parseFloat,
serialize: v => v.toString()
}
```
9. Missing eq for Reference Types
```tsx
// β clearOnDefault won't work correctly
const dateParser = {
parse: (v) => new Date(v),
serialize: (d) => d.toISOString()
}
// β Provide eq function for reference types
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: '?...' }) |
More from this repository6
three-js skill from noklip-io/agent-skills
Provides comprehensive development guidance and reference for working with Payload CMS, covering configuration, setup, and best practices for the standalone version.
Guides React developers through React 19's key patterns, breaking changes, and new paradigms for modern component and application development.
Enables high-performance web animations with powerful sequencing, scroll triggers, and extensive plugin support for complex interactive experiences.
theatre-js skill from noklip-io/agent-skills
Generates a base shadcn/ui React component setup with TypeScript, Tailwind CSS, and essential configurations for rapid UI development.