AGENTS.md
# nuqs
**Version 1.2.0**
Community
July 2026
---
## Abstract
Comprehensive best practices guide for nuqs (type-safe URL query state management) in Next.js and other React frameworks, designed for AI agents and LLMs. Covers nuqs v2.5–v2.9 features including limitUrlUpdates (built-in debounce/throttle), key isolation, Standard Schema integration, defaultOptions on NuqsAdapter (with history added in v2.9), processUrlSearchParams middleware (adapter and createSerializer), the createLoader server utility, the React Router v8 adapter, and Next.js 16 cacheComponents compatibility. Contains 39 rules across 8 categories, prioritized by impact from critical (parser configuration, adapter setup) to incremental (advanced patterns). Each rule names the wrong default it corrects, with realistic examples and, where relevant, honest consequence-based impact notes to guide automated refactoring and code generation.
---
## Table of Contents
1. [Parser Configuration](references/_sections.md#1-parser-configuration) — **CRITICAL**
- 1.1 [Choose Correct Array Parser Format](references/parser-array-format.md) — CRITICAL (prevents API integration failures from wrong URL format)
- 1.2 [Select Appropriate Date Parser](references/parser-date-format.md) — CRITICAL (prevents timezone bugs and parsing failures)
- 1.3 [Use Enum Parsers for Constrained Values](references/parser-enum-validation.md) — CRITICAL (prevents invalid state from URL manipulation)
- 1.4 [Use parseAsIndex for 1-Based URL Display](references/parser-index-offset.md) — HIGH (eliminates off-by-one errors between URL and code)
- 1.5 [Use Typed Parsers for Non-String Values](references/parser-use-typed-parsers.md) — CRITICAL (prevents runtime type errors and hydration mismatches)
- 1.6 [Use withDefault for Non-Nullable State](references/parser-with-default.md) — CRITICAL (eliminates null checks throughout component tree)
- 1.7 [Validate JSON Parser Input](references/parser-json-validation.md) — CRITICAL (prevents runtime crashes and unsafe casts from URL-supplied JSON)
2. [Adapter & Setup](references/_sections.md#2-adapter-&-setup) — **CRITICAL**
- 2.1 [Add 'use client' Directive for Hooks](references/setup-use-client.md) — CRITICAL (prevents build-breaking hook errors in RSC)
- 2.2 [Configure App-Wide Defaults on NuqsAdapter](references/setup-default-options.md) — MEDIUM (avoids repeating .withOptions on every parser; enforces consistent behaviour)
- 2.3 [Define Shared Parsers in Dedicated File](references/setup-shared-parsers.md) — HIGH (prevents parser mismatch bugs between components)
- 2.4 [Ensure Compatible Next.js Version](references/setup-nextjs-version.md) — CRITICAL (prevents cryptic runtime errors from version mismatch)
- 2.5 [Import Server Utilities from nuqs/server](references/setup-import-server.md) — CRITICAL (prevents RSC-to-client boundary contamination errors)
- 2.6 [Wrap App with NuqsAdapter](references/setup-nuqs-adapter.md) — CRITICAL (prevents 100% of hook failures from missing provider)
3. [State Management](references/_sections.md#3-state-management) — **HIGH**
- 3.1 [Avoid Derived State from URL Parameters](references/state-avoid-derived.md) — HIGH (prevents sync bugs and unnecessary re-renders)
- 3.2 [Clear URL Parameters with null](references/state-clear-with-null.md) — HIGH (reduces URL clutter by removing unnecessary parameters)
- 3.3 [Use Setter Return Value for URL Access](references/state-setter-return.md) — MEDIUM (enables accurate URL tracking for analytics/sharing without re-deriving the URL)
- 3.4 [Use Standard Schema for Cross-Library Validation](references/state-standard-schema.md) — MEDIUM (one parser map validates nuqs, tRPC, route validators, and forms — no duplicated schema)
- 3.5 [Use useQueryStates for Related Parameters](references/state-use-query-states.md) — HIGH (gives a single typed object and one combined URLSearchParams flush)
- 3.6 [Use withOptions for Parser-Level Configuration](references/state-options-inheritance.md) — MEDIUM (reduces boilerplate and ensures consistent behavior)
4. [Server Integration](references/_sections.md#4-server-integration) — **HIGH**
- 4.1 [Call parse() Before get() in Server Components](references/server-parse-before-get.md) — HIGH (prevents undefined values and runtime errors)
- 4.2 [Handle Async searchParams in Next.js 15+](references/server-next15-async.md) — HIGH (prevents build errors in Next.js 15 with async props)
- 4.3 [Integrate useTransition for Loading States](references/server-use-transition.md) — HIGH (exposes pending state for non-shallow server fetches so the UI can show loading)
- 4.4 [Use createSearchParamsCache for Server Components](references/server-search-params-cache.md) — HIGH (eliminates prop drilling across N component levels)
- 4.5 [Use shallow:false to Trigger Server Re-renders](references/server-shallow-false.md) — HIGH (enables server-side data refetching on URL change)
5. [Performance Optimization](references/_sections.md#5-performance-optimization) — **MEDIUM**
- 5.1 [Debounce Search Input Before URL Update](references/perf-debounce-search.md) — HIGH (reduces server requests during typing from N per keystroke to 1 per pause)
- 5.2 [Memoize Components Using URL State](references/perf-avoid-rerender.md) — MEDIUM (prevents unnecessary re-renders on URL changes (Next.js especially))
- 5.3 [Rely on Key Isolation Outside Next.js](references/perf-key-isolation.md) — HIGH (avoids unnecessary memoization on adapters that already scope re-renders per key)
- 5.4 [Throttle Rapid URL Updates](references/perf-throttle-updates.md) — MEDIUM (prevents browser history API rate limiting on rapid input)
- 5.5 [Use clearOnDefault for Clean URLs](references/perf-clear-on-default.md) — MEDIUM (reduces URL length by 20-50% for default values)
- 5.6 [Use createSerializer for Link URLs](references/perf-serialize-utility.md) — MEDIUM (enables SSR-compatible URL generation without hooks)
6. [History & Navigation](references/_sections.md#6-history-&-navigation) — **MEDIUM**
- 6.1 [Choose the Right history Mode (push vs replace)](references/history-push-navigation.md) — MEDIUM (back button behaves as users expect for navigation vs ephemeral state)
- 6.2 [Control Scroll Behavior on URL Changes](references/history-scroll-behavior.md) — MEDIUM (prevents jarring scroll jumps on state changes)
7. [Debugging & Testing](references/_sections.md#7-debugging-&-testing) — **LOW-MEDIUM**
- 7.1 [Enable Debug Logging for Troubleshooting](references/debug-enable-logging.md) — LOW-MEDIUM (surfaces the exact parse/serialize/URL-write step instead of guessing from silent state)
- 7.2 [Test Components with URL State](references/debug-testing.md) — LOW-MEDIUM (enables reliable CI/CD testing of nuqs components)
8. [Advanced Patterns](references/_sections.md#8-advanced-patterns) — **LOW**
- 8.1 [Create Custom Parsers for Complex Types](references/advanced-custom-parsers.md) — LOW (prevents runtime errors from string coercion)
- 8.2 [Implement eq Function for Object Parsers](references/advanced-eq-function.md) — LOW (prevents unnecessary URL updates for equivalent objects)
- 8.3 [Use Framework-Specific Adapters](references/advanced-framework-adapters.md) — LOW (prevents URL sync failures in non-Next.js apps)
- 8.4 [Use processUrlSearchParams for Canonical URL Shape](references/advanced-process-url-search-params.md) — LOW-MEDIUM (enables stable URL ordering for SEO and cache hit-rate)
- 8.5 [Use urlKeys for Shorter URLs](references/advanced-url-keys.md) — LOW (keeps shareable links compact by shortening verbose URL keys)
---
## References
1. [https://nuqs.dev](https://nuqs.dev)
2. [https://nuqs.dev/blog/nuqs-2.5](https://nuqs.dev/blog/nuqs-2.5)
3. [https://github.com/47ng/nuqs/releases/tag/v2.9.0](https://github.com/47ng/nuqs/releases/tag/v2.9.0)
4. [https://github.com/47ng/nuqs](https://github.com/47ng/nuqs)
5. [https://nextjs.org/docs](https://nextjs.org/docs)
6. [https://react.dev](https://react.dev)
7. [https://standardschema.dev](https://standardschema.dev)
---
## Source Files
This document was compiled from individual reference files. For detailed editing or extension:
| File | Description |
|------|-------------|
| [references/_sections.md](references/_sections.md) | Category definitions and ordering |
| [assets/templates/_template.md](assets/templates/_template.md) | Template for creating new rules |
| [SKILL.md](SKILL.md) | Quick reference entry point |
| [metadata.json](metadata.json) | Version and reference URLs |
assets/templates/_template.md
---
title: {Rule Title}
impact: {CRITICAL|HIGH|MEDIUM-HIGH|MEDIUM|LOW-MEDIUM|LOW}
impactDescription: {Quantified impact, e.g., "2-10× improvement", "200ms savings"}
tags: {prefix}, {technique}, {tool-if-mentioned}, {related-concepts}
---
## {Rule Title}
{1-3 sentences explaining WHY this matters. Focus on performance/correctness implications.}
**Incorrect ({what's wrong}):**
```tsx
{Bad code example - production-realistic, not strawman}
{// Comments explaining the cost}
```
**Correct ({what's right}):**
```tsx
{Good code example - minimal diff from incorrect}
{// Comments explaining the benefit}
```
{Optional sections as needed:}
**Alternative ({context}):**
```tsx
{Alternative approach when applicable}
```
**When NOT to use this pattern:**
- {Exception 1}
- {Exception 2}
**Benefits:**
- {Benefit 1}
- {Benefit 2}
Reference: [{Reference Title}]({Reference URL})
metadata.json
{
"version": "1.2.0",
"organization": "Community",
"technology": "nuqs",
"date": "July 2026",
"abstract": "Comprehensive best practices guide for nuqs (type-safe URL query state management) in Next.js and other React frameworks, designed for AI agents and LLMs. Covers nuqs v2.5–v2.9 features including limitUrlUpdates (built-in debounce/throttle), key isolation, Standard Schema integration, defaultOptions on NuqsAdapter (with history added in v2.9), processUrlSearchParams middleware (adapter and createSerializer), the createLoader server utility, the React Router v8 adapter, and Next.js 16 cacheComponents compatibility. Contains 39 rules across 8 categories, prioritized by impact from critical (parser configuration, adapter setup) to incremental (advanced patterns). Each rule names the wrong default it corrects, with realistic examples and, where relevant, honest consequence-based impact notes to guide automated refactoring and code generation.",
"references": [
"https://nuqs.dev",
"https://nuqs.dev/blog/nuqs-2.5",
"https://github.com/47ng/nuqs/releases/tag/v2.9.0",
"https://github.com/47ng/nuqs",
"https://nextjs.org/docs",
"https://react.dev",
"https://standardschema.dev"
],
"category": "Frontend"
}
README.md
# nuqs Best Practices for Next.js
A comprehensive best practices skill for using [nuqs](https://nuqs.dev) - type-safe URL query state management - in Next.js applications.
## Overview
This skill provides 42 rules across 8 categories to help AI agents and developers write correct, performant, and maintainable code when using nuqs for URL state management.
## Getting Started
```bash
# Install dependencies (if any)
pnpm install
# Build AGENTS.md from references
pnpm build
# Validate the skill
pnpm validate
```
## Categories
| Priority | Category | Impact | Rules |
|----------|----------|--------|-------|
| 1 | Parser Configuration | CRITICAL | 8 |
| 2 | Adapter & Setup | CRITICAL | 5 |
| 3 | State Management | HIGH | 7 |
| 4 | Server Integration | HIGH | 6 |
| 5 | Performance Optimization | MEDIUM | 5 |
| 6 | History & Navigation | MEDIUM | 4 |
| 7 | Debugging & Testing | LOW-MEDIUM | 3 |
| 8 | Advanced Patterns | LOW | 4 |
## Creating a New Rule
1. Create a new file in `references/` with the pattern `{prefix}-{slug}.md`
2. Use the template from `assets/templates/_template.md`
3. Include YAML frontmatter with `title`, `impact`, `impactDescription`, `tags`
4. Add **Incorrect** and **Correct** code examples
5. Run `pnpm build` to regenerate AGENTS.md
6. Run `pnpm validate` to check for errors
## Rule File Structure
```markdown
---
title: Rule Title
impact: CRITICAL|HIGH|MEDIUM|LOW
impactDescription: Quantified impact (e.g., "2-10× improvement")
tags: prefix, keyword1, keyword2
---
## Rule Title
Explanation of WHY this matters.
**Incorrect (annotation):**
\`\`\`tsx
// Bad code example
\`\`\`
**Correct (annotation):**
\`\`\`tsx
// Good code example
\`\`\`
Reference: [Link](url)
```
## File Naming Convention
- Rule files: `{prefix}-{descriptive-slug}.md` (e.g., `parser-use-typed-parsers.md`)
- Prefix must match a section defined in `references/_sections.md`
- Use lowercase with hyphens
## Impact Levels
| Level | Description |
|-------|-------------|
| CRITICAL | Causes build failures, runtime errors, or major performance issues |
| HIGH | Significant bugs, performance degradation, or maintenance problems |
| MEDIUM | Noticeable issues or suboptimal patterns |
| LOW-MEDIUM | Minor improvements or edge case handling |
| LOW | Nice-to-have optimizations or advanced patterns |
## Scripts
- `pnpm build` - Regenerate AGENTS.md from references
- `pnpm validate` - Check skill against quality guidelines
## Contributing
1. Check existing rules to avoid duplication
2. Follow the rule template structure
3. Include production-realistic code examples
4. Quantify impact where possible
5. Run validation before submitting
## References
- [nuqs Documentation](https://nuqs.dev)
- [nuqs GitHub](https://github.com/47ng/nuqs)
- [Next.js Documentation](https://nextjs.org/docs)
references/_sections.md
# Sections
This file defines all sections, their ordering, impact levels, and descriptions.
The section ID (in parentheses) is the filename prefix used to group rules.
---
## 1. Parser Configuration (parser)
**Impact:** CRITICAL
**Description:** Incorrect parsers cause type mismatches, runtime errors, and hydration failures. Parser selection cascades through the entire state lifecycle.
## 2. Adapter & Setup (setup)
**Impact:** CRITICAL
**Description:** Missing NuqsAdapter or incorrect setup causes hooks to fail silently or throw. Foundation for all nuqs functionality.
## 3. State Management (state)
**Impact:** HIGH
**Description:** Proper use of useQueryState vs useQueryStates, default values, and null handling prevents unnecessary complexity and bugs.
## 4. Server Integration (server)
**Impact:** HIGH
**Description:** Server cache and shallow routing configuration determines whether state changes trigger expensive server re-renders.
## 5. Performance Optimization (perf)
**Impact:** MEDIUM
**Description:** Throttling, batching, and update coalescing prevent browser rate-limiting and reduce unnecessary URL updates.
## 6. History & Navigation (history)
**Impact:** MEDIUM
**Description:** History mode selection affects UX - push vs replace impacts back button behavior and navigation experience.
## 7. Debugging & Testing (debug)
**Impact:** LOW-MEDIUM
**Description:** Debug logging, testing strategies, and common error diagnosis enable faster development cycles.
## 8. Advanced Patterns (advanced)
**Impact:** LOW
**Description:** Custom parsers, serializers, URL key mapping for complex use cases requiring careful implementation.
references/advanced-custom-parsers.md
---
title: Create Custom Parsers for Complex Types
impact: LOW
impactDescription: prevents runtime errors from string coercion
tags: advanced, createParser, custom, serialize, parse
---
## Create Custom Parsers for Complex Types
When built-in parsers don't fit your needs, create custom parsers with `createParser`. Define `parse`, `serialize`, and optionally `eq` for equality checking.
**Incorrect (manual parsing in component):**
```tsx
'use client'
import { useQueryState } from 'nuqs'
interface SortState {
id: string
desc: boolean
}
export default function SortableTable() {
const [sortRaw, setSortRaw] = useQueryState('sort')
// Manual parsing scattered across component
const sort: SortState = sortRaw
? { id: sortRaw.split(':')[0], desc: sortRaw.split(':')[1] === 'desc' }
: { id: 'name', desc: false }
const handleSort = (id: string) => {
// Manual serialization
setSortRaw(`${id}:${sort.id === id && !sort.desc ? 'desc' : 'asc'}`)
}
}
```
**Correct (custom parser):**
```tsx
'use client'
import { useQueryState, createParser } from 'nuqs'
interface SortState {
id: string
desc: boolean
}
const parseAsSort = createParser<SortState>({
parse(query) {
const [id = '', direction = ''] = query.split(':')
return { id, desc: direction === 'desc' }
},
serialize(value) {
return `${value.id}:${value.desc ? 'desc' : 'asc'}`
},
eq(a, b) {
return a.id === b.id && a.desc === b.desc
}
})
export default function SortableTable() {
const [sort, setSort] = useQueryState(
'sort',
parseAsSort.withDefault({ id: 'name', desc: false })
)
// Type-safe, reusable, with proper equality checking
}
```
**Typing parser-returning helpers (v2.7+):**
If you write a function that returns a custom parser (e.g. for a factory), type the return as `SingleParserBuilder<T>`. The older `ParserBuilder<T>` symbol is deprecated and will be removed in v3.
```tsx
import { createParser, type SingleParserBuilder } from 'nuqs'
export function parseAsTuple<A, B>(
a: SingleParserBuilder<A>,
b: SingleParserBuilder<B>
): SingleParserBuilder<[A, B]> {
return createParser<[A, B]>({
parse(query) {
const [left, right] = query.split('|')
const A = a.parse(left ?? '')
const B = b.parse(right ?? '')
return A === null || B === null ? null : [A, B]
},
serialize([A, B]) {
return `${a.serialize(A)}|${b.serialize(B)}`
}
})
}
```
**For object/JSON-shaped values:** prefer `parseAsJson` with a Standard Schema (Zod, Valibot, ArkType, Effect Schema) over a hand-rolled `createParser` — see `parser-json-validation` and `state-standard-schema`.
Reference: [nuqs Custom Parsers](https://nuqs.dev/docs/parsers/making-your-own)
references/advanced-eq-function.md
---
title: Implement eq Function for Object Parsers
impact: LOW
impactDescription: prevents unnecessary URL updates for equivalent objects
tags: advanced, eq, equality, objects, optimization
---
## Implement eq Function for Object Parsers
When creating custom parsers for objects, implement the `eq` function to define equality. Without it, nuqs uses reference equality, causing unnecessary URL updates for equivalent but different object instances.
**Incorrect (reference equality):**
```tsx
import { createParser } from 'nuqs'
interface Filters {
minPrice: number
maxPrice: number
}
const parseAsFilters = createParser<Filters>({
parse(query) {
const [min, max] = query.split('-').map(Number)
return { minPrice: min, maxPrice: max }
},
serialize({ minPrice, maxPrice }) {
return `${minPrice}-${maxPrice}`
}
// Missing eq - uses reference equality
})
// Problem: setting same values creates new object references
setFilters({ minPrice: 0, maxPrice: 100 })
setFilters({ minPrice: 0, maxPrice: 100 }) // Triggers URL update even though values are same
```
**Correct (value equality):**
```tsx
import { createParser } from 'nuqs'
interface Filters {
minPrice: number
maxPrice: number
}
const parseAsFilters = createParser<Filters>({
parse(query) {
const [min, max] = query.split('-').map(Number)
return { minPrice: min, maxPrice: max }
},
serialize({ minPrice, maxPrice }) {
return `${minPrice}-${maxPrice}`
},
eq(a, b) {
return a.minPrice === b.minPrice && a.maxPrice === b.maxPrice
}
})
// Now same values don't trigger unnecessary updates
setFilters({ minPrice: 0, maxPrice: 100 })
setFilters({ minPrice: 0, maxPrice: 100 }) // No URL update - values are equal
```
**For arrays:**
```tsx
const parseAsIdList = createParser<number[]>({
parse(query) {
return query.split(',').map(Number)
},
serialize(value) {
return value.join(',')
},
eq(a, b) {
return a.length === b.length && a.every((v, i) => v === b[i])
}
})
```
**For nested objects:**
```tsx
import isEqual from 'lodash/isEqual' // or deep-equal
const parseAsConfig = createParser<Config>({
parse: JSON.parse,
serialize: JSON.stringify,
eq: isEqual // Deep equality comparison
})
```
Reference: [nuqs Custom Parsers](https://nuqs.dev/docs/parsers/making-your-own)
references/advanced-framework-adapters.md
---
title: Use Framework-Specific Adapters
impact: LOW
impactDescription: prevents URL sync failures in non-Next.js apps
tags: advanced, adapters, remix, react-router, tanstack-router, frameworks
---
## Use Framework-Specific Adapters
nuqs works with multiple React frameworks through adapters. Use the correct adapter for your framework to ensure proper URL synchronisation. Picking the wrong adapter usually fails silently — hooks return stale values or never update the URL.
**Incorrect (Next.js adapter inside a React Router tree):**
```tsx
// src/main.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app' // Wrong runtime
import { BrowserRouter } from 'react-router-dom'
function App() {
return (
<BrowserRouter>
<NuqsAdapter>
<Routes />
</NuqsAdapter>
</BrowserRouter>
)
}
```
**Correct (React Router v6 adapter):**
```tsx
// src/main.tsx
import { NuqsAdapter } from 'nuqs/adapters/react-router/v6'
import { BrowserRouter } from 'react-router-dom'
function App() {
return (
<BrowserRouter>
<NuqsAdapter>
<Routes />
</NuqsAdapter>
</BrowserRouter>
)
}
```
**Available adapters (current as of nuqs v2.9):**
| Framework | Import Path | Notes |
|-----------|-------------|-------|
| Next.js App Router | `nuqs/adapters/next/app` | |
| Next.js Pages Router | `nuqs/adapters/next/pages` | |
| Next.js (unified) | `nuqs/adapters/next` | Use when an app mixes both routers |
| React Router v6 | `nuqs/adapters/react-router/v6` | |
| React Router v7 | `nuqs/adapters/react-router/v7` | |
| React Router v8 | `nuqs/adapters/react-router/v8` | Added in v2.9 |
| Remix | `nuqs/adapters/remix` | |
| TanStack Router | `nuqs/adapters/tanstack-router` | Added in v2.5 |
| Plain React (no router) | `nuqs/adapters/react` | For Vite / CRA apps with no router |
| Testing | `nuqs/adapters/testing` | See `debug-testing` |
**Deprecation:** The dedicated `react-router/v5` subpath was removed in v2.9 (v5 apps use the unversioned alias or upgrade). The unversioned `nuqs/adapters/react-router` import (which still aliases v6) is itself deprecated and slated for removal in nuqs v3 — always pin `/v6`, `/v7`, or `/v8` explicitly.
**Key isolation (v2.5+):** All non-Next.js adapters scope re-renders to the specific URL key a hook subscribes to. Next.js continues to re-render the entire subtree on any URL change because its `URLSearchParams` context is global. If fine-grained re-renders matter and you're not on Next.js, you generally don't need to memoize aggressively. See `perf-avoid-rerender`.
Reference: [nuqs Adapters](https://nuqs.dev/docs/adapters)
references/advanced-process-url-search-params.md
---
title: Use processUrlSearchParams for Canonical URL Shape
impact: LOW-MEDIUM
impactDescription: enables stable URL ordering for SEO and cache hit-rate
tags: advanced, processUrlSearchParams, seo, canonical-url, createSerializer
---
## Use processUrlSearchParams for Canonical URL Shape
nuqs v2.6 added `processUrlSearchParams`, a middleware that transforms the `URLSearchParams` object **just before** it is written to the URL (when passed as an `NuqsAdapter` prop) or **just before** the URL is serialised (when passed to `createSerializer`). The most common use is to canonicalise key ordering — without it, `?b=2&a=1` and `?a=1&b=2` are two different cache keys for the same logical query, hurting SEO and CDN hit-rate.
**Incorrect (key order leaks from React render order):**
```tsx
// User toggles filters in different orders → different URLs for the same query
// /search?tag=react&sort=desc
// /search?sort=desc&tag=react
// Both render the same results, but each is a distinct canonical URL for Google.
```
**Correct (sort keys via adapter middleware):**
```tsx
// app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<NuqsAdapter
processUrlSearchParams={(params) => {
// Sort keys alphabetically before each write
params.sort()
return params
}}
>
{children}
</NuqsAdapter>
</body>
</html>
)
}
```
**For SEO canonical URLs in `generateMetadata` — same hook on `createSerializer`:**
```tsx
// lib/searchParams.ts
import { createSerializer, parseAsString, parseAsInteger } from 'nuqs/server'
export const searchParamsMap = {
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1)
}
export const serializeSearch = createSerializer(searchParamsMap, {
processUrlSearchParams(params) {
params.sort()
return params
}
})
// app/search/page.tsx
import type { Metadata } from 'next'
import { serializeSearch } from '@/lib/searchParams'
export async function generateMetadata({ searchParams }): Promise<Metadata> {
const params = await searchParams
return {
alternates: {
canonical: serializeSearch('/search', params)
}
}
}
```
**Other uses:**
- Stripping internal tracking params (`utm_*`) before write.
- Coercing booleans to a stable string form (`true` vs. `1`).
- Removing empty values that didn't get caught by `clearOnDefault` for legacy reasons.
**When NOT to use this pattern:**
- Single-page app with no SEO concerns — the indirection costs more than the wobble.
- You need to drop keys that affect server-side parsing — strip them on the server (in `createSearchParamsCache`/`createLoader`) instead, where the parser map is the source of truth.
Reference: [nuqs Options](https://nuqs.dev/docs/options)
references/advanced-url-keys.md
---
title: Use urlKeys for Shorter URLs
impact: LOW
impactDescription: keeps shareable links compact by shortening verbose URL keys
tags: advanced, urlKeys, serializer, url-length, abbreviation
---
## Use urlKeys for Shorter URLs
Map verbose parameter names to shorter URL keys for cleaner, more shareable URLs while keeping descriptive names in code.
**Incorrect (verbose URL parameters):**
```tsx
'use client'
import { useQueryStates, parseAsFloat, parseAsInteger } from 'nuqs'
export default function MapView() {
const [coords, setCoords] = useQueryStates({
latitude: parseAsFloat.withDefault(0),
longitude: parseAsFloat.withDefault(0),
zoomLevel: parseAsInteger.withDefault(10)
})
// URL: ?latitude=48.8566&longitude=2.3522&zoomLevel=12
// Long, harder to share, uses more bandwidth
return <Map {...coords} />
}
```
**Correct (abbreviated URL keys):**
```tsx
'use client'
import { useQueryStates, parseAsFloat, parseAsInteger } from 'nuqs'
export default function MapView() {
const [coords, setCoords] = useQueryStates(
{
latitude: parseAsFloat.withDefault(0),
longitude: parseAsFloat.withDefault(0),
zoomLevel: parseAsInteger.withDefault(10)
},
{
urlKeys: {
latitude: 'lat',
longitude: 'lng',
zoomLevel: 'z'
}
}
)
// URL: ?lat=48.8566&lng=2.3522&z=12
// Shorter, cleaner URLs
// Code still uses descriptive names
console.log(coords.latitude, coords.longitude, coords.zoomLevel)
return <Map {...coords} />
}
```
Reference: [nuqs urlKeys](https://nuqs.dev/docs/utilities)
references/debug-enable-logging.md
---
title: Enable Debug Logging for Troubleshooting
impact: LOW-MEDIUM
impactDescription: surfaces the exact parse/serialize/URL-write step instead of guessing from silent state
tags: debug, logging, localStorage, troubleshooting, devtools
---
## Enable Debug Logging for Troubleshooting
Enable nuqs debug logs to understand state changes, URL updates, and timing. Useful for diagnosing issues with state synchronization or unexpected behavior.
**Incorrect (no visibility into nuqs operations):**
```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Counter() {
const [count, setCount] = useQueryState('count', parseAsInteger.withDefault(0))
// Something's not working, but no way to see what nuqs is doing
// Have to guess and add console.logs everywhere
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
```
**Correct (enable debug logging):**
```javascript
// Run in browser DevTools console FIRST
localStorage.debug = 'nuqs'
// Then reload the page
// Now you see: [nuqs] useQueryState 'count' initialized with 0
// And: [nuqs] useQueryState 'count' updated to 1
```
**Disable when done:**
```javascript
// Run in browser DevTools console
delete localStorage.debug
```
**Performance timing markers:**
Debug mode also records User Timing markers visible in the Performance tab:
- `nuqs:parse` - Time to parse URL parameters
- `nuqs:serialize` - Time to serialize state to URL
- `nuqs:update` - Time for URL update
Reference: [nuqs Debugging](https://nuqs.dev/docs)
references/debug-testing.md
---
title: Test Components with URL State
impact: LOW-MEDIUM
impactDescription: enables reliable CI/CD testing of nuqs components
tags: debug, testing, jest, vitest, react-testing-library
---
## Test Components with URL State
Test components that use nuqs by providing the NuqsTestingAdapter and controlling URL state in tests.
**Incorrect (test fails without adapter):**
```tsx
// components/Pagination.test.tsx
import { render, screen } from '@testing-library/react'
import Pagination from './Pagination'
it('displays current page', () => {
render(<Pagination />) // Fails: no NuqsAdapter
expect(screen.getByText('Page 1')).toBeInTheDocument()
})
```
**Correct (use NuqsTestingAdapter):**
```tsx
// test/utils.tsx
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
import { render, type RenderOptions } from '@testing-library/react'
export function renderWithNuqs(
ui: React.ReactElement,
{ searchParams = {}, ...options }: RenderOptions & { searchParams?: Record<string, string> } = {}
) {
return render(
<NuqsTestingAdapter searchParams={searchParams}>
{ui}
</NuqsTestingAdapter>,
options
)
}
// components/Pagination.test.tsx
import { screen } from '@testing-library/react'
import { renderWithNuqs } from '@/test/utils'
import Pagination from './Pagination'
it('displays current page from URL', () => {
renderWithNuqs(<Pagination />, { searchParams: { page: '5' } })
expect(screen.getByText('Page 5')).toBeInTheDocument()
})
```
Reference: [nuqs Testing Adapter](https://nuqs.dev/docs/testing)
references/history-push-navigation.md
---
title: Choose the Right history Mode (push vs replace)
impact: MEDIUM
impactDescription: back button behaves as users expect for navigation vs ephemeral state
tags: history, push, replace, navigation, back-button, ux
---
## Choose the Right history Mode (push vs replace)
`history: 'replace'` is the default: state updates rewrite the current history entry, so the back button never walks through intermediate values. Reach for `history: 'push'` only when a state change *is* navigation the user should be able to undo (pagination, tabs, modal state). Getting this backwards produces two opposite bugs — a back button that leaves the site, or one that is unusable.
**Use `history: 'push'` for navigation-like state:**
```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Pagination() {
const [page, setPage] = useQueryState(
'page',
parseAsInteger.withDefault(1).withOptions({ history: 'push' })
)
// Page 1 → 2 → 3, then Back returns to page 2 (not off-site)
return (
<nav>
<button onClick={() => setPage(p => p - 1)}>Previous</button>
<span>Page {page}</span>
<button onClick={() => setPage(p => p + 1)}>Next</button>
</nav>
)
}
```
Typical `push` cases: pagination, tab selection, modal open/close, step-by-step wizards, filter-panel expansion.
**Keep the default `replace` for ephemeral state:**
```tsx
'use client'
import { useQueryState, parseAsString } from 'nuqs'
export default function SearchBox() {
// No withOptions — replace is the default
const [query, setQuery] = useQueryState('q', parseAsString.withDefault(''))
// Typing "react" does NOT create entries r, re, rea, reac, react
return <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search…" />
}
```
Using `history: 'push'` here would push one entry per keystroke and make the back button useless. Typical `replace` cases: search input text, slider/range values, real-time filters, sort order — any rapidly-changing state.
**Mix modes on a per-call basis when the same key does both:**
```tsx
const [page, setPage] = useQueryState(
'page',
parseAsInteger.withDefault(1).withOptions({ history: 'push' })
)
setPage(5) // navigation → pushes
setPage(1, { history: 'replace' }) // "reset to first page" shouldn't spam Back
```
The mirror pattern also works: keep the parser on `replace`, mirror the input in local `useState` while typing, and `setQuery(input, { history: 'push' })` only on explicit submit.
Reference: [nuqs History Option](https://nuqs.dev/docs/options)
references/history-scroll-behavior.md
---
title: Control Scroll Behavior on URL Changes
impact: MEDIUM
impactDescription: prevents jarring scroll jumps on state changes
tags: history, scroll, ux, navigation, viewport
---
## Control Scroll Behavior on URL Changes
By default, nuqs doesn't scroll on URL changes. Use the `scroll` option to control whether state changes scroll to the top of the page.
**Incorrect (unwanted scroll on filter change):**
```tsx
'use client'
import { useQueryState, parseAsString } from 'nuqs'
export default function FilterPanel() {
const [filter, setFilter] = useQueryState('filter', parseAsString.withDefault('').withOptions({
scroll: true // Bad for filters - user loses their place!
}))
return (
<select value={filter} onChange={e => setFilter(e.target.value)}>
<option value="">All</option>
<option value="active">Active</option>
</select>
)
}
```
**Correct (no scroll for filters, scroll for pagination):**
```tsx
'use client'
import { useQueryState, parseAsString, parseAsInteger } from 'nuqs'
export default function SearchPage() {
// No scroll for filters - user stays in place
const [filter, setFilter] = useQueryState('filter', parseAsString.withDefault(''))
// Scroll for pagination - user sees new content from top
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1).withOptions({
scroll: true,
history: 'push'
}))
return (
<div>
<select value={filter} onChange={e => setFilter(e.target.value)}>
<option value="">All</option>
<option value="active">Active</option>
</select>
<button onClick={() => setPage(p => p + 1)}>Next Page</button>
</div>
)
}
```
Reference: [nuqs Scroll Option](https://nuqs.dev/docs/options)
references/parser-array-format.md
---
title: Choose Correct Array Parser Format
impact: CRITICAL
impactDescription: prevents API integration failures from wrong URL format
tags: parser, parseAsArrayOf, parseAsNativeArrayOf, arrays, url-format
---
## Choose Correct Array Parser Format
nuqs offers two array formats with different URL representations. Choose based on your backend API expectations and URL readability requirements.
**Incorrect (wrong format for backend):**
```tsx
'use client'
import { useQueryState, parseAsArrayOf, parseAsString } from 'nuqs'
export default function TagFilter() {
const [tags, setTags] = useQueryState(
'tags',
parseAsArrayOf(parseAsString).withDefault([])
)
// URL: ?tags=react,nextjs
// But backend expects: ?tag=react&tag=nextjs
// API receives single string "react,nextjs" instead of array!
}
```
**Correct (match backend expectations):**
```tsx
'use client'
import { useQueryState, parseAsNativeArrayOf, parseAsString } from 'nuqs'
export default function TagFilter() {
const [tags, setTags] = useQueryState(
'tag', // Note: singular key name
parseAsNativeArrayOf(parseAsString).withDefault([])
)
// URL: ?tag=react&tag=nextjs
// Backend correctly receives array ['react', 'nextjs']
return (
<div>
Tags: {tags.join(', ')}
<button onClick={() => setTags([...tags, 'new-tag'])}>Add</button>
</div>
)
}
```
**When to use each:**
| Format | URL Example | Use When | Min version |
|--------|-------------|----------|-------------|
| `parseAsArrayOf` | `?ids=1,2,3` | Compact URLs, numeric IDs, custom backends | All versions |
| `parseAsNativeArrayOf` | `?tag=a&tag=b` | Standard form encoding, PHP/Rails backends | **v2.7+** (MultiParsers) |
If you're on nuqs < 2.7, use `parseAsArrayOf` with a separator change (e.g., `.withOptions({ ... })`) or upgrade — there is no shim.
Reference: [nuqs Array Parsers](https://nuqs.dev/docs/parsers/built-in)
references/parser-date-format.md
---
title: Select Appropriate Date Parser
impact: CRITICAL
impactDescription: prevents timezone bugs and parsing failures
tags: parser, parseAsTimestamp, parseAsIsoDateTime, parseAsIsoDate, dates
---
## Select Appropriate Date Parser
nuqs provides three date parsers with different URL formats and precision. Choose based on your requirements for time precision and URL readability.
**Incorrect (wrong parser for use case):**
```tsx
'use client'
import { useQueryState, parseAsIsoDateTime } from 'nuqs'
export default function DateRangePicker() {
const [startDate, setStartDate] = useQueryState('start', parseAsIsoDateTime)
// URL: ?start=2024-01-01T00:00:00.000Z
// Problem: User selected "Jan 1" but URL shows timezone complexity
// Better: Use parseAsIsoDate for date-only pickers
return (
<input
type="date"
value={startDate?.toISOString().slice(0, 10) ?? ''}
onChange={e => setStartDate(new Date(e.target.value))}
/>
)
}
```
**Correct (match parser to use case):**
```tsx
'use client'
import { useQueryState, parseAsIsoDate, parseAsTimestamp } from 'nuqs'
export default function DateRangePicker() {
// For date-only picker: clean URL
const [startDate, setStartDate] = useQueryState('start', parseAsIsoDate)
// URL: ?start=2024-01-01
// For precise timestamps: use parseAsTimestamp
const [lastModified, setLastModified] = useQueryState('modified', parseAsTimestamp)
// URL: ?modified=1704067200000
return (
<input
type="date"
value={startDate?.toISOString().slice(0, 10) ?? ''}
onChange={e => setStartDate(new Date(e.target.value))}
/>
)
}
```
**When to use each:**
| Parser | URL Format | Use Case |
|--------|------------|----------|
| `parseAsTimestamp` | `1704067200000` | Precise timestamps, API integration |
| `parseAsIsoDateTime` | `2024-01-01T12:00:00.000Z` | Debugging, shareable URLs with time |
| `parseAsIsoDate` | `2024-01-01` | Date pickers, calendar views |
Reference: [nuqs Date Parsers](https://nuqs.dev/docs/parsers)
references/parser-enum-validation.md
---
title: Use Enum Parsers for Constrained Values
impact: CRITICAL
impactDescription: prevents invalid state from URL manipulation
tags: parser, parseAsStringEnum, parseAsStringLiteral, validation, security
---
## Use Enum Parsers for Constrained Values
When state should only accept specific values (like status, sort direction, or view mode), use enum or literal parsers. This prevents invalid values from URL tampering and provides type safety.
**Incorrect (accepts any string):**
```tsx
'use client'
import { useQueryState } from 'nuqs'
type SortOrder = 'asc' | 'desc'
export default function SortableList() {
const [sort, setSort] = useQueryState('sort')
// sort is string | null - accepts ANY value
// URL: ?sort=malicious works silently
const sortOrder = sort as SortOrder // Unsafe cast!
return (
<select
value={sort ?? 'asc'}
onChange={e => setSort(e.target.value)}
>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
)
}
```
**Correct (validated enum):**
```tsx
'use client'
import { useQueryState, parseAsStringLiteral } from 'nuqs'
const sortOrders = ['asc', 'desc'] as const
export default function SortableList() {
const [sort, setSort] = useQueryState(
'sort',
parseAsStringLiteral(sortOrders).withDefault('asc')
)
// sort is 'asc' | 'desc' - invalid values return null/default
// URL: ?sort=malicious → falls back to 'asc'
return (
<select value={sort} onChange={e => setSort(e.target.value as typeof sort)}>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
)
}
```
**Alternative (TypeScript enum):**
```tsx
import { parseAsStringEnum } from 'nuqs'
enum Status {
Active = 'active',
Inactive = 'inactive',
Pending = 'pending'
}
const [status, setStatus] = useQueryState(
'status',
parseAsStringEnum<Status>(Object.values(Status)).withDefault(Status.Active)
)
```
Reference: [nuqs Enum Parsers](https://nuqs.dev/docs/parsers)
references/parser-index-offset.md
---
title: Use parseAsIndex for 1-Based URL Display
impact: HIGH
impactDescription: eliminates off-by-one errors between URL and code
tags: parser, parseAsIndex, pagination, zero-indexed, one-indexed
---
## Use parseAsIndex for 1-Based URL Display
Arrays are 0-indexed in JavaScript, but users expect 1-indexed URLs (page 1, item 1). `parseAsIndex` automatically converts between them.
**Incorrect (manual conversion):**
```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Pagination() {
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
// URL: ?page=1
// Array index: need page - 1 everywhere
const items = ['a', 'b', 'c', 'd', 'e']
const currentItem = items[page - 1] // Manual conversion
return (
<div>
<p>Page {page}: {currentItem}</p>
<button onClick={() => setPage(p => p + 1)}>Next</button>
</div>
)
}
```
**Correct (automatic conversion):**
```tsx
'use client'
import { useQueryState, parseAsIndex } from 'nuqs'
export default function Pagination() {
const [pageIndex, setPageIndex] = useQueryState(
'page',
parseAsIndex.withDefault(0)
)
// URL: ?page=1 (user-friendly, 1-indexed)
// State: 0 (code-friendly, 0-indexed)
const items = ['a', 'b', 'c', 'd', 'e']
const currentItem = items[pageIndex] // Direct array access
return (
<div>
<p>Page {pageIndex + 1}: {currentItem}</p>
<button onClick={() => setPageIndex(i => i + 1)}>Next</button>
</div>
)
}
```
**How it works:**
- URL `?page=1` → State `0`
- URL `?page=5` → State `4`
- State `0` → URL `?page=1`
- State `4` → URL `?page=5`
**Benefits:**
- No off-by-one bugs
- Array indices work directly
- URLs are human-friendly
Reference: [nuqs parseAsIndex](https://nuqs.dev/docs/parsers)
references/parser-json-validation.md
---
title: Validate JSON Parser Input
impact: CRITICAL
impactDescription: prevents runtime crashes and unsafe casts from URL-supplied JSON
tags: parser, parseAsJson, validation, standard-schema, zod
---
## Validate JSON Parser Input
`parseAsJson` requires a validator function (this is a hard requirement in nuqs v2 — calling `parseAsJson<T>()` with no argument is a TypeScript error). The validator must return the typed value when valid and either throw or return `null` when invalid. Avoid the "make the type errors go away" shortcut of an unchecked cast — URL params are attacker-controlled input, and an unchecked cast lets any shape into your app.
**Incorrect (unchecked cast as the validator):**
```tsx
'use client'
import { useQueryState, parseAsJson } from 'nuqs'
interface Filters {
minPrice: number
maxPrice: number
categories: string[]
}
export default function FilterPanel() {
const [filters, setFilters] = useQueryState(
'filters',
parseAsJson((v) => v as Filters) // Cast — no runtime check
)
// URL: ?filters={"minPrice":"haha"} → filters.minPrice is "haha", not a number
// URL: ?filters=notjson → null, but downstream code expecting a string crashes
return <div>Min: {filters?.minPrice.toFixed(2)}</div>
}
```
**Correct (hand-rolled type guard):**
```tsx
'use client'
import { useQueryState, parseAsJson } from 'nuqs'
interface Filters {
minPrice: number
maxPrice: number
categories: string[]
}
function isFilters(value: unknown): value is Filters {
if (!value || typeof value !== 'object') return false
const obj = value as Record<string, unknown>
return (
typeof obj.minPrice === 'number' &&
typeof obj.maxPrice === 'number' &&
Array.isArray(obj.categories) &&
obj.categories.every((c) => typeof c === 'string')
)
}
export default function FilterPanel() {
const [filters] = useQueryState(
'filters',
parseAsJson<Filters>((value) => (isFilters(value) ? value : null)).withDefault({
minPrice: 0,
maxPrice: 1000,
categories: []
})
)
// Invalid JSON → null → falls back to default
return <div>Price: {filters.minPrice} – {filters.maxPrice}</div>
}
```
**Correct (Standard Schema — Zod, Valibot, ArkType, Effect Schema):**
Since nuqs v2.5 the validator slot accepts any Standard Schema parser directly — no `safeParse(...).success ? value : null` wrapper required. Zod's `.parse` throws on invalid input, which nuqs catches and converts to `null`.
```tsx
import { z } from 'zod'
const FiltersSchema = z.object({
minPrice: z.number(),
maxPrice: z.number(),
categories: z.array(z.string())
})
const [filters] = useQueryState(
'filters',
parseAsJson(FiltersSchema.parse).withDefault({
minPrice: 0,
maxPrice: 1000,
categories: []
})
)
```
**When NOT to use this pattern:**
- For non-object types, prefer typed primitive parsers (`parseAsInteger`, `parseAsStringEnum`, `parseAsArrayOf`) — they're cheaper and the validator is implicit.
Reference: [nuqs Built-in Parsers](https://nuqs.dev/docs/parsers/built-in)
references/parser-use-typed-parsers.md
---
title: Use Typed Parsers for Non-String Values
impact: CRITICAL
impactDescription: prevents runtime type errors and hydration mismatches
tags: parser, type-safety, parseAsInteger, parseAsFloat, parseAsBoolean
---
## Use Typed Parsers for Non-String Values
URL query parameters are always strings. Without typed parsers, you'll get string values where you expect numbers or booleans, causing type errors and incorrect comparisons.
**Incorrect (string instead of number):**
```tsx
'use client'
import { useQueryState } from 'nuqs'
export default function Pagination() {
const [page, setPage] = useQueryState('page')
// page is string | null, not number
// page + 1 = "11" not 2 when page is "1"
return (
<button onClick={() => setPage(String(Number(page) + 1))}>
Next Page
</button>
)
}
```
**Correct (typed parser):**
```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Pagination() {
const [page, setPage] = useQueryState('page', parseAsInteger)
// page is number | null
// Arithmetic works correctly
return (
<button onClick={() => setPage((p) => (p ?? 0) + 1)}>
Next Page
</button>
)
}
```
**Available parsers:**
- `parseAsInteger` - integers
- `parseAsFloat` - decimal numbers
- `parseAsBoolean` - true/false
- `parseAsTimestamp` - Date from milliseconds
- `parseAsIsoDateTime` - Date from ISO string
- `parseAsJson<T>()` - JSON objects
Reference: [nuqs Parsers Documentation](https://nuqs.dev/docs/parsers)
references/parser-with-default.md
---
title: Use withDefault for Non-Nullable State
impact: CRITICAL
impactDescription: eliminates null checks throughout component tree
tags: parser, withDefault, null-safety, type-inference
---
## Use withDefault for Non-Nullable State
Without `withDefault`, query state is always nullable (`T | null`). This forces null checks everywhere the value is used. Use `withDefault` to provide a fallback value and get non-nullable types.
**Incorrect (nullable state, null checks everywhere):**
```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Counter() {
const [count, setCount] = useQueryState('count', parseAsInteger)
// count is number | null
return (
<div>
{/* Null check required */}
<p>Count: {count ?? 0}</p>
{/* Null check required */}
<button onClick={() => setCount((count ?? 0) + 1)}>
Increment
</button>
</div>
)
}
```
**Correct (non-nullable with default):**
```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Counter() {
const [count, setCount] = useQueryState(
'count',
parseAsInteger.withDefault(0)
)
// count is number (never null)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</div>
)
}
```
**Benefits:**
- TypeScript infers non-nullable type
- No null coalescing needed
- Functional updates work without null checks
- Default appears in URL only when `clearOnDefault: false`
Reference: [nuqs withDefault](https://nuqs.dev/docs/parsers)
references/perf-avoid-rerender.md
---
title: Memoize Components Using URL State
impact: MEDIUM
impactDescription: prevents unnecessary re-renders on URL changes (Next.js especially)
tags: perf, memo, re-renders, key-isolation, react
---
## Memoize Components Using URL State
When URL state changes, components subscribed to that state re-render. On Next.js, every `useQueryState`/`useQueryStates` consumer in the subtree re-renders on **any** URL change because `URLSearchParams` is provided through a single context. Use `React.memo` and confine hook usage to leaf components to avoid cascading re-renders.
On non-Next.js adapters (React SPA, React Router, Remix, TanStack Router), nuqs v2.5 added **key isolation**: a hook only re-renders when its specific key changes. The memoization advice below is still useful but far less critical on those frameworks — see `perf-key-isolation`.
**Incorrect (entire page re-renders on every Next.js URL change):**
```tsx
'use client'
import { useQueryState, parseAsString } from 'nuqs'
export default function SearchPage() {
const [query, setQuery] = useQueryState('q', parseAsString.withDefault(''))
return (
<div>
<SearchInput query={query} setQuery={setQuery} />
<ExpensiveSidebar /> {/* Re-renders on every query change */}
<ResultsList query={query} />
</div>
)
}
function ExpensiveSidebar() {
// Heavy computation that doesn't need query
return <aside>…</aside>
}
```
**Correct (memoize unrelated subtrees):**
```tsx
'use client'
import { memo } from 'react'
import { useQueryState, parseAsString } from 'nuqs'
export default function SearchPage() {
const [query, setQuery] = useQueryState('q', parseAsString.withDefault(''))
return (
<div>
<SearchInput query={query} setQuery={setQuery} />
<ExpensiveSidebar /> {/* Memoized — bypasses re-render when query changes */}
<ResultsList query={query} />
</div>
)
}
const ExpensiveSidebar = memo(function ExpensiveSidebar() {
return <aside>…</aside>
})
```
**Better (hoist the hook into a leaf — works on all adapters):**
```tsx
'use client'
import { useQueryState, parseAsString } from 'nuqs'
function SearchSection() {
const [query, setQuery] = useQueryState('q', parseAsString.withDefault(''))
return (
<>
<SearchInput query={query} setQuery={setQuery} />
<ResultsList query={query} />
</>
)
}
export default function SearchPage() {
return (
<div>
<SearchSection /> {/* Only this subtree re-renders on URL change */}
<ExpensiveSidebar /> {/* Not affected */}
</div>
)
}
```
**When NOT to use this pattern:**
- Non-Next.js adapters where the consumer is already a leaf — key isolation makes memoization unnecessary noise.
Reference: [React memo](https://react.dev/reference/react/memo)
references/perf-clear-on-default.md
---
title: Use clearOnDefault for Clean URLs
impact: MEDIUM
impactDescription: reduces URL length by 20-50% for default values
tags: perf, clearOnDefault, url-cleanup, defaults, seo
---
## Use clearOnDefault for Clean URLs
By default, nuqs removes parameters from the URL when they match the default value. This keeps URLs clean. Set `clearOnDefault: false` only when you need the parameter always visible.
**Incorrect (default always shown in URL):**
```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Pagination() {
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1).withOptions({
clearOnDefault: false // Unnecessary!
}))
// URL always shows ?page=1 even on first page
// Clutters shareable URLs, hurts SEO
return <button onClick={() => setPage(1)}>First</button>
}
```
**Correct (clean URLs by default):**
```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Pagination() {
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
// clearOnDefault: true (default)
// page=1: URL is /search (clean)
// page=2: URL is /search?page=2
return (
<div>
<button onClick={() => setPage(1)}>First</button>
<button onClick={() => setPage(p => p + 1)}>Next</button>
</div>
)
}
```
**When clearOnDefault: false is appropriate:**
- Analytics tracking requires all parameters
- API expects explicit parameter even for default
- Debugging where you need to see all state
Reference: [nuqs clearOnDefault](https://nuqs.dev/docs/options)
references/perf-debounce-search.md
---
title: Debounce Search Input Before URL Update
impact: HIGH
impactDescription: reduces server requests during typing from N per keystroke to 1 per pause
tags: perf, limitUrlUpdates, debounce, search, server-load
---
## Debounce Search Input Before URL Update
For search inputs that drive a server fetch (`shallow: false`), debounce the URL write so the server only re-renders once the user stops typing. Since nuqs v2.5, debounce is built in — pass `limitUrlUpdates: debounce(N)` instead of hand-rolling a `setTimeout` dance with a parallel local-state mirror.
**Incorrect (a server request per keystroke):**
```tsx
'use client'
import { useQueryState, parseAsString } from 'nuqs'
export default function SearchBox() {
const [query, setQuery] = useQueryState(
'q',
parseAsString.withDefault('').withOptions({
shallow: false // Every keystroke triggers a server re-render
})
)
return <input value={query} onChange={(e) => setQuery(e.target.value)} />
}
```
**Correct (built-in debounce — one request per pause):**
```tsx
'use client'
import { useQueryState, parseAsString, debounce } from 'nuqs'
import { useTransition } from 'react'
export default function SearchBox() {
const [isLoading, startTransition] = useTransition()
const [query, setQuery] = useQueryState(
'q',
parseAsString.withDefault('').withOptions({
shallow: false,
startTransition,
limitUrlUpdates: debounce(300) // Server request fires 300ms after last keystroke
})
)
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{isLoading && <span>Searching…</span>}
</>
)
}
```
**Why this beats the manual `setTimeout` pattern:**
- One source of truth — `query` is both the displayed value and the in-flight value; no parallel `useState` mirror.
- The Promise returned by the setter still resolves to the merged `URLSearchParams` after the debounce fires, so analytics/share flows keep working.
- nuqs v2.6 emits a warning if you combine `shallow: true` with debounce — `shallow: true` already keeps the URL local, so debouncing it is almost always a bug. Honour the warning.
**Alternative (no URL-driven fetch — useDeferredValue is fine):**
When the search is purely client-side (`shallow: true`, the default), React's `useDeferredValue` keeps the input responsive without touching nuqs options:
```tsx
'use client'
import { useDeferredValue } from 'react'
import { useQueryState, parseAsString } from 'nuqs'
export default function ClientSearch() {
const [query, setQuery] = useQueryState('q', parseAsString.withDefault(''))
const deferredQuery = useDeferredValue(query)
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Results query={deferredQuery} />
</>
)
}
```
**When NOT to use this pattern:**
- The setter only fires on submit/blur — there's nothing to debounce.
- You want to rate-limit continuous input (slider drags, scrub bars) — `throttle()` keeps intermediate values; `debounce()` discards them. See `perf-throttle-updates`.
Reference: [nuqs Options](https://nuqs.dev/docs/options)
references/perf-key-isolation.md
---
title: Rely on Key Isolation Outside Next.js
impact: HIGH
impactDescription: avoids unnecessary memoization on adapters that already scope re-renders per key
tags: perf, key-isolation, react-router, tanstack-router, remix, re-renders
---
## Rely on Key Isolation Outside Next.js
nuqs v2.5 added **key isolation** on every non-Next.js adapter (Plain React, React Router v5/v6/v7, Remix, TanStack Router). A `useQueryState('foo', …)` consumer there only re-renders when **`foo`** changes — unrelated URL key changes are filtered out by the adapter's subscription model. On those frameworks, the manual `memo`/leaf-component gymnastics from `perf-avoid-rerender` are usually unnecessary.
Next.js still re-renders the whole subtree on any URL change because `URLSearchParams` is delivered via a single root context. Treat Next.js as the exception.
**Incorrect (over-memoising on a key-isolated adapter):**
```tsx
// React Router v7 app
import { memo } from 'react'
import { useQueryState, parseAsString } from 'nuqs'
const Tags = memo(function Tags() {
const [tags] = useQueryState('tags', parseAsString.withDefault(''))
return <TagBadges tags={tags} />
})
const Sort = memo(function Sort() {
const [sort] = useQueryState('sort', parseAsString.withDefault('desc'))
return <SortPicker value={sort} />
})
// `memo` is redundant — Tags never re-renders when `sort` changes,
// and vice versa. Adapter already isolates by key.
```
**Correct (let the adapter do the work):**
```tsx
// React Router v7 app
import { useQueryState, parseAsString } from 'nuqs'
function Tags() {
const [tags] = useQueryState('tags', parseAsString.withDefault(''))
return <TagBadges tags={tags} />
}
function Sort() {
const [sort] = useQueryState('sort', parseAsString.withDefault('desc'))
return <SortPicker value={sort} />
}
// Tags re-renders only on `tags` changes, Sort only on `sort` — no wrappers needed.
```
**Practical rule of thumb:**
| Adapter | Re-render scope per `useQueryState` | Manual memoization usually worth it? |
|---------|-------------------------------------|--------------------------------------|
| Next.js (App + Pages) | Whole consumer subtree on **any** URL change | Yes (see `perf-avoid-rerender`) |
| Plain React | Only when subscribed key changes | No |
| React Router v5/v6/v7 | Only when subscribed key changes | No |
| Remix | Only when subscribed key changes | No |
| TanStack Router | Only when subscribed key changes | No |
**Caveat:** `useQueryStates` (the plural form) intentionally subscribes to **every** key in its object, so it re-renders whenever any of those keys change — that is its contract. If you want per-key isolation, prefer multiple `useQueryState` hooks.
**When NOT to use this pattern:**
- You're on Next.js — apply `perf-avoid-rerender` instead.
- You're using `useQueryStates` for atomic updates and accept the coarser subscription. See `state-use-query-states` for the tradeoff.
Reference: [nuqs 2.5 release notes](https://nuqs.dev/blog/nuqs-2.5)
references/perf-serialize-utility.md
---
title: Use createSerializer for Link URLs
impact: MEDIUM
impactDescription: enables SSR-compatible URL generation without hooks
tags: perf, createSerializer, links, navigation, ssr
---
## Use createSerializer for Link URLs
When generating URLs for links or navigation without needing state, use `createSerializer`. This avoids unnecessary hook usage and works in Server Components.
**Incorrect (hook for URL generation only):**
```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function PaginationLinks({ totalPages }: { totalPages: number }) {
const [page] = useQueryState('page', parseAsInteger.withDefault(1))
// Using state just to generate URLs
return (
<nav>
{Array.from({ length: totalPages }, (_, i) => (
<a key={i} href={`?page=${i + 1}`}>
{i + 1}
</a>
))}
</nav>
)
}
```
**Correct (serializer utility):**
```tsx
// lib/searchParams.ts
import { createSerializer, parseAsInteger, parseAsString } from 'nuqs/server'
export const searchParams = {
q: parseAsString,
page: parseAsInteger.withDefault(1)
}
export const serialize = createSerializer(searchParams)
// components/PaginationLinks.tsx (can be Server Component)
import { serialize } from '@/lib/searchParams'
export default function PaginationLinks({ totalPages }: { totalPages: number }) {
return (
<nav>
{Array.from({ length: totalPages }, (_, i) => (
<a key={i} href={`?${serialize({ page: i + 1 })}`}>
{i + 1}
</a>
))}
</nav>
)
}
```
**Building on existing URL:**
```tsx
import { serialize } from '@/lib/searchParams'
// Preserve existing params, change page
const currentParams = { q: 'react', page: 1 }
const nextPageUrl = `?${serialize({ ...currentParams, page: 2 })}`
// Result: ?q=react&page=2
```
**With base URL:**
```tsx
const url = serialize('/search', { q: 'react', page: 1 })
// Result: /search?q=react&page=1
```
Reference: [nuqs createSerializer](https://nuqs.dev/docs/utilities)
references/perf-throttle-updates.md
---
title: Throttle Rapid URL Updates
impact: MEDIUM
impactDescription: prevents browser history API rate limiting on rapid input
tags: perf, limitUrlUpdates, throttle, rate-limiting, slider
---
## Throttle Rapid URL Updates
Browsers rate-limit History API calls. Rapid updates (typing, sliders, dragging) can exceed this limit, causing dropped updates and console warnings. Pass `limitUrlUpdates: throttle(N)` to coalesce URL writes — local state continues to update instantly, the URL writes at most every N ms.
The legacy `throttleMs: N` option was deprecated in nuqs v2.5. It still works in v2.9 for backwards compatibility, but its JSDoc marks it deprecated and it will be removed in v3 — write new code with `limitUrlUpdates: throttle(N)` (or `debounce(N)`).
**Incorrect (every keystroke pushes to history):**
```tsx
'use client'
import { useQueryState, parseAsString } from 'nuqs'
export default function SearchBox() {
const [query, setQuery] = useQueryState('q', parseAsString.withDefault(''))
// Every keystroke writes to history → browser may throttle after ~100 rapid updates
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
)
}
```
**Correct (throttle URL updates):**
```tsx
'use client'
import { useQueryState, parseAsString, throttle } from 'nuqs'
export default function SearchBox() {
const [query, setQuery] = useQueryState(
'q',
parseAsString.withDefault('').withOptions({
limitUrlUpdates: throttle(300) // URL flushed at most every 300ms
})
)
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
)
}
```
**For sliders and drag operations (tighter window):**
```tsx
'use client'
import { useQueryState, parseAsInteger, throttle } from 'nuqs'
export default function VolumeSlider() {
const [volume, setVolume] = useQueryState(
'volume',
parseAsInteger.withDefault(50).withOptions({
limitUrlUpdates: throttle(100) // More responsive for continuous input
})
)
return (
<input
type="range" min={0} max={100}
value={volume}
onChange={(e) => setVolume(Number(e.target.value))}
/>
)
}
```
**Force an immediate (non-throttled) flush on a single call:**
```tsx
import { defaultRateLimit } from 'nuqs'
// Normal updates use the throttle window
setQuery('intermediate')
// On blur, bypass rate limiting and commit immediately
setQuery('final value', { limitUrlUpdates: defaultRateLimit })
```
**When NOT to use this pattern:**
- Search inputs that drive a server fetch (`shallow: false`) — prefer `debounce()` so the request only fires once the user stops typing. See `perf-debounce-search`.
- One-off updates (button clicks, form submits) — there's nothing to throttle.
**Note:** UI state from the hook updates synchronously regardless of `limitUrlUpdates`; only the URL write is rate-limited.
Reference: [nuqs Options](https://nuqs.dev/docs/options)
references/server-next15-async.md
---
title: Handle Async searchParams in Next.js 15+
impact: HIGH
impactDescription: prevents build errors in Next.js 15 with async props
tags: server, nextjs15, async, searchParams, promise
---
## Handle Async searchParams in Next.js 15+
In Next.js 15+, `searchParams` is a Promise that must be awaited. Using it directly without await causes TypeScript errors and runtime issues.
**Incorrect (Next.js 15+ without await):**
```tsx
// app/search/page.tsx
import { searchParamsCache } from '@/lib/searchParams'
type PageProps = {
searchParams: { q?: string } // Wrong type for Next.js 15+
}
export default async function SearchPage({ searchParams }: PageProps) {
// searchParams is a Promise, not an object
const { q } = searchParamsCache.parse(searchParams) // Type error
}
```
**Correct (Next.js 15+):**
```tsx
// app/search/page.tsx
import { searchParamsCache } from '@/lib/searchParams'
import type { SearchParams } from 'nuqs/server'
type PageProps = {
searchParams: Promise<SearchParams> // Correct type
}
export default async function SearchPage({ searchParams }: PageProps) {
// Await the Promise
const { q, page } = await searchParamsCache.parse(searchParams)
return <Results query={q} page={page} />
}
```
**For Next.js 14 and earlier:**
```tsx
// app/search/page.tsx
import { searchParamsCache } from '@/lib/searchParams'
type PageProps = {
searchParams: Record<string, string | string[] | undefined>
}
export default function SearchPage({ searchParams }: PageProps) {
// No await needed in Next.js 14
const { q, page } = searchParamsCache.parse(searchParams)
return <Results query={q} page={page} />
}
```
**Version-agnostic pattern:**
```tsx
import type { SearchParams } from 'nuqs/server'
type PageProps = {
searchParams: Promise<SearchParams>
}
export default async function SearchPage({ searchParams }: PageProps) {
// Works in both Next.js 14 and 15
const { q } = await searchParamsCache.parse(searchParams)
}
```
Reference: [Next.js 15 Migration](https://nextjs.org/docs/app/building-your-application/upgrading/version-15)
references/server-parse-before-get.md
---
title: Call parse() Before get() in Server Components
impact: HIGH
impactDescription: prevents undefined values and runtime errors
tags: server, parse, get, searchParamsCache, initialization
---
## Call parse() Before get() in Server Components
`createSearchParamsCache` requires calling `parse()` at the page level before calling `get()` in nested components. Forgetting `parse()` causes `get()` to return undefined or throw.
**Incorrect (missing parse):**
```tsx
// app/search/page.tsx
import { ResultsHeader } from './ResultsHeader'
export default async function SearchPage({ searchParams }) {
// Missing: await searchParamsCache.parse(searchParams)
return (
<div>
<ResultsHeader /> {/* Will fail or return undefined */}
<Results />
</div>
)
}
// components/ResultsHeader.tsx
import { searchParamsCache } from '@/lib/searchParams'
export function ResultsHeader() {
const query = searchParamsCache.get('q') // Error: cache not initialized
return <h1>Results for {query}</h1>
}
```
**Correct (parse at page level):**
```tsx
// app/search/page.tsx
import { searchParamsCache } from '@/lib/searchParams'
import { ResultsHeader } from './ResultsHeader'
import type { SearchParams } from 'nuqs/server'
type PageProps = {
searchParams: Promise<SearchParams>
}
export default async function SearchPage({ searchParams }: PageProps) {
// Parse FIRST, before rendering any children
await searchParamsCache.parse(searchParams)
return (
<div>
<ResultsHeader /> {/* Now get() works */}
<Results />
</div>
)
}
// components/ResultsHeader.tsx
import { searchParamsCache } from '@/lib/searchParams'
export function ResultsHeader() {
const query = searchParamsCache.get('q') // Works correctly
return <h1>Results for {query}</h1>
}
```
**Parse and destructure in one step:**
```tsx
export default async function SearchPage({ searchParams }: PageProps) {
const { q, page } = await searchParamsCache.parse(searchParams)
// Use q and page directly, or let children use get()
return <Results query={q} page={page} />
}
```
Reference: [nuqs Server Cache](https://nuqs.dev/docs/server-side)
references/server-search-params-cache.md
---
title: Use createSearchParamsCache for Server Components
impact: HIGH
impactDescription: eliminates prop drilling across N component levels
tags: server, createSearchParamsCache, server-components, type-safety
---
## Use createSearchParamsCache for Server Components
In Server Components, use `createSearchParamsCache` to access URL parameters with type safety. This avoids prop drilling and provides the same parsers as client-side hooks.
**Incorrect (manual parsing):**
```tsx
// app/search/page.tsx
type PageProps = {
searchParams: Promise<{ q?: string; page?: string }>
}
export default async function SearchPage({ searchParams }: PageProps) {
const params = await searchParams
const query = params.q ?? ''
const page = parseInt(params.page ?? '1', 10) // Manual parsing
// No type safety, parsing logic duplicated
return <Results query={query} page={page} />
}
```
**Correct (search params cache):**
```tsx
// lib/searchParams.ts
import {
createSearchParamsCache,
parseAsString,
parseAsInteger
} from 'nuqs/server'
export const searchParamsCache = createSearchParamsCache({
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1)
})
// app/search/page.tsx
import { searchParamsCache } from '@/lib/searchParams'
import type { SearchParams } from 'nuqs/server'
type PageProps = {
searchParams: Promise<SearchParams>
}
export default async function SearchPage({ searchParams }: PageProps) {
// Parse once at page level
const { q, page } = await searchParamsCache.parse(searchParams)
return <Results query={q} page={page} />
}
```
**Access in nested Server Components:**
```tsx
// components/ResultsHeader.tsx
import { searchParamsCache } from '@/lib/searchParams'
export function ResultsHeader() {
// No props needed - access from cache
const query = searchParamsCache.get('q')
const page = searchParamsCache.get('page')
return <h1>Results for "{query}" (Page {page})</h1>
}
```
**Important:** Call `parse()` once at the page level before using `get()` in nested components.
**Alternative: `createLoader` for non-nested cases.** If you only consume the parsed values inside the page itself (no deeply nested Server Components needing `get()`), `createLoader` is the lighter primitive — it returns a single function and skips the React-cache plumbing. Use the cache when you'd otherwise prop-drill; use the loader when you wouldn't.
```tsx
// lib/searchParams.ts
import { createLoader, parseAsString, parseAsInteger } from 'nuqs/server'
export const loadSearchParams = createLoader({
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1)
})
// app/search/page.tsx
import { loadSearchParams } from '@/lib/searchParams'
export default async function SearchPage({ searchParams }) {
const { q, page } = await loadSearchParams(searchParams)
return <Results query={q} page={page} />
}
```
The same parser map can power **both** `createSearchParamsCache` and `createLoader` (and a client-side `useQueryStates`), so the choice is purely about ergonomics — see `setup-shared-parsers`.
Reference: [nuqs Server-Side](https://nuqs.dev/docs/server-side)
references/server-shallow-false.md
---
title: Use shallow:false to Trigger Server Re-renders
impact: HIGH
impactDescription: enables server-side data refetching on URL change
tags: server, shallow, server-components, data-fetching, rsc
---
## Use shallow:false to Trigger Server Re-renders
By default, nuqs updates are client-side only (`shallow: true`). Set `shallow: false` to trigger Server Component re-renders when URL changes, enabling server-side data fetching.
**Incorrect (server data never refreshes):**
```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Pagination() {
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
// shallow: true (default) - server doesn't see URL changes
// Server-fetched data stays stale
return <button onClick={() => setPage(p => p + 1)}>Next</button>
}
```
**Correct (server refetches on change):**
```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Pagination() {
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1).withOptions({
shallow: false // Notify server of URL changes
}))
// Server Components re-render with new page value
return <button onClick={() => setPage(p => p + 1)}>Next</button>
}
```
**With loading state using useTransition:**
```tsx
'use client'
import { useTransition } from 'react'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Pagination() {
const [isLoading, startTransition] = useTransition()
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1).withOptions({
shallow: false,
startTransition // Shows loading during server fetch
}))
return (
<div>
{isLoading && <span>Loading...</span>}
<button onClick={() => setPage(p => p + 1)} disabled={isLoading}>
Next
</button>
</div>
)
}
```
**When to use shallow:false:**
- Pagination with server-fetched data
- Search that triggers server queries
- Filters that affect server-rendered content
- Any state that affects Server Component output
Reference: [nuqs Shallow Option](https://nuqs.dev/docs/options)
references/server-use-transition.md
---
title: Integrate useTransition for Loading States
impact: HIGH
impactDescription: exposes pending state for non-shallow server fetches so the UI can show loading
tags: server, useTransition, loading, suspense, streaming
---
## Integrate useTransition for Loading States
When using `shallow: false`, integrate React's `useTransition` to track when the server is fetching new data. This enables loading indicators during URL-triggered server updates.
**Incorrect (no loading feedback):**
```tsx
'use client'
import { useQueryState, parseAsString } from 'nuqs'
export default function SearchBox() {
const [query, setQuery] = useQueryState('q', parseAsString.withDefault('').withOptions({
shallow: false
}))
// User types, waits with no feedback while server fetches
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
)
}
```
**Correct (loading state):**
```tsx
'use client'
import { useTransition } from 'react'
import { useQueryState, parseAsString } from 'nuqs'
export default function SearchBox() {
const [isLoading, startTransition] = useTransition()
const [query, setQuery] = useQueryState('q', parseAsString.withDefault('').withOptions({
shallow: false,
startTransition
}))
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
{isLoading && <span className="spinner" />}
</div>
)
}
```
**With disabled interaction during load:**
```tsx
'use client'
import { useTransition } from 'react'
import { useQueryStates, parseAsString, parseAsInteger } from 'nuqs'
export default function FilterPanel() {
const [isLoading, startTransition] = useTransition()
const [filters, setFilters] = useQueryStates(
{
category: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1)
},
{
shallow: false,
startTransition
}
)
return (
<fieldset disabled={isLoading}>
<select
value={filters.category}
onChange={e => setFilters({ category: e.target.value, page: 1 })}
>
<option value="">All</option>
<option value="electronics">Electronics</option>
</select>
{isLoading && <p>Updating results...</p>}
</fieldset>
)
}
```
Reference: [nuqs useTransition Integration](https://nuqs.dev/docs/options)
references/setup-default-options.md
---
title: Configure App-Wide Defaults on NuqsAdapter
impact: MEDIUM
impactDescription: avoids repeating .withOptions on every parser; enforces consistent behaviour
tags: setup, NuqsAdapter, defaultOptions, configuration, consistency
---
## Configure App-Wide Defaults on NuqsAdapter
Since nuqs v2.5, `NuqsAdapter` accepts a `defaultOptions` prop. It takes exactly the serialisable options — `shallow`, `scroll`, `clearOnDefault`, `limitUrlUpdates`, and (since v2.9.0) `history`. Setting them once on the adapter eliminates the temptation to copy-paste the same `.withOptions({ … })` chain onto every parser, and reduces the chance one component silently disagrees with the rest of the app (e.g. one missing `shallow: false` that drops a server fetch). `startTransition` is **not** a `defaultOptions` key — it's a function from `useTransition()` and can only be passed per hook call.
**Incorrect (every parser repeats the same options):**
```tsx
// Search page
const [q] = useQueryState('q', parseAsString.withDefault('').withOptions({ shallow: false, scroll: false }))
const [page] = useQueryState('page', parseAsInteger.withDefault(1).withOptions({ shallow: false, scroll: false }))
// Filters page
const [tag] = useQueryState('tag', parseAsString.withDefault('').withOptions({ shallow: false /* forgot scroll: false */ }))
// Inconsistency: filter changes now scroll to top; search changes don't.
```
**Correct (set defaults once on the adapter):**
```tsx
// app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<NuqsAdapter
defaultOptions={{
shallow: false, // Every URL change re-runs the server
scroll: false // Never auto-scroll on URL update
}}
>
{children}
</NuqsAdapter>
</body>
</html>
)
}
```
```tsx
// Anywhere in the tree — no per-parser duplication
const [q] = useQueryState('q', parseAsString.withDefault(''))
const [page] = useQueryState('page', parseAsInteger.withDefault(1))
// Both inherit shallow: false, scroll: false from the adapter
```
**Per-hook overrides still work:**
```tsx
// A purely-local UI flag — opt out of the server round-trip just here
const [drawerOpen, setDrawerOpen] = useQueryState(
'drawer',
parseAsBoolean.withDefault(false).withOptions({ shallow: true })
)
```
**Precedence (low → high):** built-in defaults → adapter `defaultOptions` → parser `.withOptions(…)` → per-call `setX(value, { … })`.
**When NOT to use this pattern:**
- The app has fewer than a handful of nuqs hooks — duplicating options is fine.
- Different sections of the app legitimately want different defaults (e.g. an embedded widget vs. a full page) — split the tree into two adapters with their own `defaultOptions`.
Reference: [nuqs 2.5 release notes](https://nuqs.dev/blog/nuqs-2.5)
references/setup-import-server.md
---
title: Import Server Utilities from nuqs/server
impact: CRITICAL
impactDescription: prevents RSC-to-client boundary contamination errors
tags: setup, nuqs/server, imports, createSearchParamsCache, server-components
---
## Import Server Utilities from nuqs/server
Server-side utilities like `createSearchParamsCache` must be imported from `nuqs/server`, not `nuqs`. The main `nuqs` export includes the `'use client'` directive which contaminates Server Components.
**Incorrect (wrong import):**
```tsx
// lib/searchParams.ts
import { createSearchParamsCache, parseAsString } from 'nuqs'
// Error: This import adds 'use client' to your server code
export const searchParamsCache = createSearchParamsCache({
q: parseAsString.withDefault('')
})
```
**Correct (server import):**
```tsx
// lib/searchParams.ts
import {
createSearchParamsCache,
parseAsString,
parseAsInteger
} from 'nuqs/server'
// No 'use client' directive - safe for Server Components
export const searchParamsCache = createSearchParamsCache({
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1)
})
```
**Usage in Server Component:**
```tsx
// app/search/page.tsx
import { searchParamsCache } from '@/lib/searchParams'
import type { SearchParams } from 'nuqs/server'
type PageProps = {
searchParams: Promise<SearchParams>
}
export default async function SearchPage({ searchParams }: PageProps) {
const { q, page } = await searchParamsCache.parse(searchParams)
return (
<div>
<h1>Results for: {q}</h1>
<p>Page: {page}</p>
</div>
)
}
```
**What to import from where:**
| Import | Source | Use In |
|--------|--------|--------|
| `useQueryState`, `useQueryStates` | `nuqs` | Client Components |
| `createSearchParamsCache` | `nuqs/server` | Server Components |
| Parsers (`parseAsString`, etc.) | `nuqs/server` for server, `nuqs` for client | Either |
Reference: [nuqs Server-Side](https://nuqs.dev/docs/server-side)
references/setup-nextjs-version.md
---
title: Ensure Compatible Next.js Version
impact: CRITICAL
impactDescription: prevents cryptic runtime errors from version mismatch
tags: setup, nextjs, version, compatibility, app-router
---
## Ensure Compatible Next.js Version
`nuqs@^2` declares `next` as a peer dependency at `>=14.2.0` — the **same floor for both App and Pages routers**. There is no separate, lower minimum for the Pages Router; older tables that list `12.0.0` or `14.0.0` are wrong for v2. On Next.js below 14.2, install `nuqs@^1` instead. Using an unsupported combination surfaces as cryptic runtime errors, not a clean install failure.
**Version Requirements (nuqs v2):**
| Next.js | Support | Notes |
|---------|---------|-------|
| `< 14.2.0` | Not supported by nuqs v2 | Use `nuqs@^1` for these versions. |
| `>= 14.2.0` | App & Pages routers | Minimum for `nuqs@^2` (both routers). |
| `15.x` | App & Pages routers | `searchParams` is `Promise<SearchParams>` — must be `await`-ed. See `server-next15-async`. |
| `16.x` (`cacheComponents`) | App router | Requires **nuqs `>= 2.9.0`** to avoid stale URL reads when Server Components are cached. Older nuqs returns outdated query values on revisit. |
**Check your version:**
```bash
npm list next
# or
yarn why next
# or
pnpm why next
```
**Incorrect (outdated Next.js):**
```json
{
"dependencies": {
"next": "13.5.0",
"nuqs": "^2.0.0"
}
}
// May cause: "Cannot read property 'push' of undefined"
// Or: URL updates not reflected
```
**Correct (compatible version):**
```json
{
"dependencies": {
"next": "14.2.0",
"nuqs": "^2.0.0"
}
}
```
**Upgrade command:**
```bash
npm install next@latest
# or
yarn add next@latest
# or
pnpm add next@latest
```
**Common symptoms of version mismatch:**
- `useQueryState` returns undefined
- URL doesn't update on state change
- Hydration mismatches
- `TypeError: Cannot read property 'push' of undefined`
Reference: [nuqs Installation](https://nuqs.dev/docs/installation)
references/setup-nuqs-adapter.md
---
title: Wrap App with NuqsAdapter
impact: CRITICAL
impactDescription: prevents 100% of hook failures from missing provider
tags: setup, NuqsAdapter, provider, app-router, pages-router
---
## Wrap App with NuqsAdapter
nuqs requires the `NuqsAdapter` provider to function. Without it, `useQueryState` hooks won't sync with the URL and may throw errors.
**Incorrect (missing adapter):**
```tsx
// src/app/layout.tsx
export default function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html>
<body>{children}</body>
</html>
)
}
// useQueryState calls will fail silently or throw
```
**Correct (App Router):**
```tsx
// src/app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app'
export default function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
)
}
```
**Correct (Pages Router):**
```tsx
// src/pages/_app.tsx
import type { AppProps } from 'next/app'
import { NuqsAdapter } from 'nuqs/adapters/next/pages'
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<NuqsAdapter>
<Component {...pageProps} />
</NuqsAdapter>
)
}
```
**Available adapters (nuqs v2.9+):**
- `nuqs/adapters/next/app` — Next.js App Router
- `nuqs/adapters/next/pages` — Next.js Pages Router
- `nuqs/adapters/next` — Next.js unified (mixed routers)
- `nuqs/adapters/react` — Plain React (no router, e.g. Vite/CRA)
- `nuqs/adapters/remix` — Remix
- `nuqs/adapters/react-router/v6` — React Router v6
- `nuqs/adapters/react-router/v7` — React Router v7
- `nuqs/adapters/react-router/v8` — React Router v8 (added v2.9)
- `nuqs/adapters/tanstack-router` — TanStack Router (added v2.5)
- `nuqs/adapters/testing` — Tests (see `debug-testing`)
The dedicated `react-router/v5` adapter subpath was removed in v2.9 — v5 apps import the unversioned `nuqs/adapters/react-router` (which still aliases v6) or upgrade. That bare alias is itself deprecated and slated for removal in v3, so pin `/v6`, `/v7`, or `/v8` explicitly.
Reference: [nuqs Adapters](https://nuqs.dev/docs/adapters)
references/setup-shared-parsers.md
---
title: Define Shared Parsers in Dedicated File
impact: HIGH
impactDescription: prevents parser mismatch bugs between components
tags: setup, parsers, organization, reusability, consistency
---
## Define Shared Parsers in Dedicated File
When multiple components use the same URL parameters, define parsers in a shared file. This prevents mismatches where one component uses `parseAsInteger` and another uses `parseAsString` for the same parameter.
**Incorrect (duplicate parser definitions):**
```tsx
// components/Pagination.tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export function Pagination() {
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
return <button onClick={() => setPage(p => p + 1)}>Next</button>
}
// components/PageInfo.tsx
'use client'
import { useQueryState } from 'nuqs'
export function PageInfo() {
const [page] = useQueryState('page') // String parser - mismatch!
return <span>Page: {page}</span> // Shows "1" not 1
}
```
**Correct (shared parsers):**
```tsx
// lib/searchParams.ts
import { parseAsInteger, parseAsString, parseAsStringLiteral } from 'nuqs'
export const searchParams = {
page: parseAsInteger.withDefault(1),
query: parseAsString.withDefault(''),
sort: parseAsStringLiteral(['asc', 'desc'] as const).withDefault('asc')
}
// components/Pagination.tsx
'use client'
import { useQueryState } from 'nuqs'
import { searchParams } from '@/lib/searchParams'
export function Pagination() {
const [page, setPage] = useQueryState('page', searchParams.page)
return <button onClick={() => setPage(p => p + 1)}>Next</button>
}
// components/PageInfo.tsx
'use client'
import { useQueryState } from 'nuqs'
import { searchParams } from '@/lib/searchParams'
export function PageInfo() {
const [page] = useQueryState('page', searchParams.page)
return <span>Page: {page}</span> // Correctly typed as number
}
```
**Share the same map between client and server:**
The same parser map drives a Server Component's `createSearchParamsCache` (or `createLoader`) and a client `useQueryState`. Keep the server cache in its own module so `nuqs/server` never leaks into a client bundle, and import the parsers from a client-safe file:
```tsx
// lib/searchParams.server.ts — server-only
import { createSearchParamsCache } from 'nuqs/server'
import { searchParams } from './searchParams'
export const searchParamsCache = createSearchParamsCache(searchParams)
```
A default that disagrees across the boundary (server `withDefault(1)`, client `withDefault(0)`) is a classic hydration mismatch — the shared map makes it impossible.
**Benefits:**
- Single source of truth for parser configuration
- TypeScript catches mismatches at compile time
- Easy to update defaults in one place
Reference: [nuqs Documentation](https://nuqs.dev/docs)
references/setup-use-client.md
---
title: Add 'use client' Directive for Hooks
impact: CRITICAL
impactDescription: prevents build-breaking hook errors in RSC
tags: setup, use-client, server-components, client-components
---
## Add 'use client' Directive for Hooks
`useQueryState` and `useQueryStates` are React hooks that require client-side rendering. Using them in Server Components causes build errors.
**Incorrect (missing directive):**
```tsx
// app/search/page.tsx
import { useQueryState } from 'nuqs'
export default function SearchPage() {
const [query, setQuery] = useQueryState('q')
// Error: Hooks can only be called inside Client Components
return <input value={query ?? ''} onChange={e => setQuery(e.target.value)} />
}
```
**Correct (client component):**
```tsx
// app/search/page.tsx
'use client'
import { useQueryState } from 'nuqs'
export default function SearchPage() {
const [query, setQuery] = useQueryState('q')
return <input value={query ?? ''} onChange={e => setQuery(e.target.value)} />
}
```
**Alternative (extract to client component):**
```tsx
// app/search/page.tsx (Server Component)
import SearchInput from './SearchInput'
export default function SearchPage() {
return (
<div>
<h1>Search</h1>
<SearchInput />
</div>
)
}
// app/search/SearchInput.tsx (Client Component)
'use client'
import { useQueryState } from 'nuqs'
export default function SearchInput() {
const [query, setQuery] = useQueryState('q')
return <input value={query ?? ''} onChange={e => setQuery(e.target.value)} />
}
```
**Note:** For reading search params in Server Components without hooks, use `createSearchParamsCache` from `nuqs/server`.
Reference: [nuqs Server-Side](https://nuqs.dev/docs/server-side)
references/state-avoid-derived.md
---
title: Avoid Derived State from URL Parameters
impact: HIGH
impactDescription: prevents sync bugs and unnecessary re-renders
tags: state, derived-state, anti-pattern, single-source-of-truth
---
## Avoid Derived State from URL Parameters
Don't copy URL state into local `useState`. This creates two sources of truth that can drift out of sync. Use the URL state directly or compute derived values.
**Incorrect (duplicated state):**
```tsx
'use client'
import { useState, useEffect } from 'react'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Pagination() {
const [urlPage] = useQueryState('page', parseAsInteger.withDefault(1))
const [page, setPage] = useState(urlPage) // Duplicated!
useEffect(() => {
setPage(urlPage) // Sync attempt - can cause loops
}, [urlPage])
return <span>Page: {page}</span>
}
```
**Correct (single source of truth):**
```tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Pagination() {
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
return <span>Page: {page}</span>
}
```
**For derived values, use useMemo:**
```tsx
'use client'
import { useMemo } from 'react'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Pagination() {
const [page] = useQueryState('page', parseAsInteger.withDefault(1))
// Derived value, not duplicated state
const isFirstPage = useMemo(() => page === 1, [page])
const pageRange = useMemo(
() => ({ start: (page - 1) * 10, end: page * 10 }),
[page]
)
return (
<div>
<span>Page: {page}</span>
{isFirstPage && <span>(First page)</span>}
</div>
)
}
```
**Exception: Debounced input**
```tsx
// OK to have local state for debounced input
const [query, setQuery] = useQueryState('q')
const [inputValue, setInputValue] = useState(query ?? '')
// Debounce URL updates
useEffect(() => {
const timeout = setTimeout(() => setQuery(inputValue || null), 300)
return () => clearTimeout(timeout)
}, [inputValue, setQuery])
```
Reference: [React Derived State](https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
references/state-clear-with-null.md
---
title: Clear URL Parameters with null
impact: HIGH
impactDescription: reduces URL clutter by removing unnecessary parameters
tags: state, null, clear, url-cleanup, reset
---
## Clear URL Parameters with null
To remove a parameter from the URL, set it to `null`. Setting to empty string (`''`) or `0` keeps the parameter in the URL with that value.
**Incorrect (empty string in URL):**
```tsx
'use client'
import { useQueryState } from 'nuqs'
export default function SearchBox() {
const [query, setQuery] = useQueryState('q')
const clear = () => setQuery('')
// URL: ?q= (empty but parameter remains)
return (
<div>
<input value={query ?? ''} onChange={e => setQuery(e.target.value)} />
<button onClick={clear}>Clear</button>
</div>
)
}
```
**Correct (null removes parameter):**
```tsx
'use client'
import { useQueryState } from 'nuqs'
export default function SearchBox() {
const [query, setQuery] = useQueryState('q')
const clear = () => setQuery(null)
// URL: / (parameter removed entirely)
return (
<div>
<input
value={query ?? ''}
onChange={e => setQuery(e.target.value || null)}
/>
<button onClick={clear}>Clear</button>
</div>
)
}
```
**Pattern: Convert empty to null on change:**
```tsx
<input
value={query ?? ''}
onChange={e => setQuery(e.target.value || null)}
/>
// Empty input → null → clean URL
// "search term" → "search term" → ?q=search+term
```
**With typed parsers:**
```tsx
const [count, setCount] = useQueryState('count', parseAsInteger)
// Clear the parameter
setCount(null) // URL: /
// With default, null resets to default behavior
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
setPage(null) // URL: / (page defaults to 1, not shown)
```
Reference: [nuqs Documentation](https://nuqs.dev/docs)
references/state-options-inheritance.md
---
title: Use withOptions for Parser-Level Configuration
impact: MEDIUM
impactDescription: reduces boilerplate and ensures consistent behavior
tags: state, withOptions, configuration, parsers, reusability
---
## Use withOptions for Parser-Level Configuration
Instead of passing options to every `useQueryState` call, configure options on the parser itself with `withOptions`. This ensures consistent behavior and reduces repetition.
**Incorrect (options repeated at every call site):**
```tsx
'use client'
import { useQueryState, parseAsString, throttle } from 'nuqs'
export default function SearchPage() {
const [query, setQuery] = useQueryState(
'q',
parseAsString.withDefault('').withOptions({ shallow: false, limitUrlUpdates: throttle(500), history: 'push' })
)
const [filter, setFilter] = useQueryState(
'filter',
parseAsString.withDefault('').withOptions({ shallow: false, limitUrlUpdates: throttle(500), history: 'push' })
)
// Same option bag copy-pasted — one typo and the two keys drift apart
}
```
**Correct (parser-level options):**
```tsx
// lib/searchParams.ts
import { parseAsString, parseAsInteger, throttle } from 'nuqs'
const serverSyncOptions = {
shallow: false,
limitUrlUpdates: throttle(500),
history: 'push' as const
}
export const searchParams = {
query: parseAsString.withDefault('').withOptions(serverSyncOptions),
filter: parseAsString.withDefault('').withOptions(serverSyncOptions),
page: parseAsInteger.withDefault(1).withOptions(serverSyncOptions)
}
// components/SearchPage.tsx
'use client'
import { useQueryState } from 'nuqs'
import { searchParams } from '@/lib/searchParams'
export default function SearchPage() {
const [query, setQuery] = useQueryState('q', searchParams.query)
const [filter, setFilter] = useQueryState('filter', searchParams.filter)
const [page, setPage] = useQueryState('page', searchParams.page)
// All use the same options consistently
}
```
**Options can be chained:**
```tsx
parseAsInteger
.withDefault(1)
.withOptions({ shallow: false })
.withOptions({ limitUrlUpdates: throttle(300) }) // Merges with previous options
```
Reference: [nuqs Options](https://nuqs.dev/docs/options)
references/state-setter-return.md
---
title: Use Setter Return Value for URL Access
impact: MEDIUM
impactDescription: enables accurate URL tracking for analytics/sharing without re-deriving the URL
tags: state, setter, return-value, URLSearchParams, analytics
---
## Use Setter Return Value for URL Access
The state setter returns a `Promise<URLSearchParams>` that resolves to the merged URL search params after the update is flushed. Use it whenever you need the resulting URL immediately (sharing, copy-to-clipboard, analytics) instead of re-deriving the URL by hand and risking drift from nuqs's own serialisation. Because it resolves to a `URLSearchParams` object, you must call `.toString()` when embedding it in a string.
**Incorrect (manually reconstructing the URL):**
```tsx
'use client'
import { useQueryState, parseAsString } from 'nuqs'
export default function ShareButton() {
const [query, setQuery] = useQueryState('q', parseAsString.withDefault(''))
const share = () => {
setQuery('shared-term')
// Manual URL construction — drifts from nuqs's encoding (clearOnDefault, urlKeys, etc.)
const url = `${window.location.pathname}?q=shared-term`
navigator.clipboard.writeText(url)
}
return <button onClick={share}>Share</button>
}
```
**Correct (use the awaited return value):**
```tsx
'use client'
import { useQueryState, parseAsString } from 'nuqs'
export default function ShareButton() {
const [query, setQuery] = useQueryState('q', parseAsString.withDefault(''))
const share = async () => {
const search = await setQuery('shared-term')
// search is URLSearchParams — call .toString() to embed in a URL.
const url = `${window.location.origin}${window.location.pathname}?${search.toString()}`
await navigator.clipboard.writeText(url)
}
return <button onClick={share}>Share</button>
}
```
**For analytics:**
```tsx
const trackSearch = async (term: string) => {
const search = await setQuery(term)
analytics.track('search', {
term,
url: `?${search.toString()}`,
// You can also pull individual keys directly off URLSearchParams:
canonicalQuery: search.get('q')
})
}
```
**With `useQueryStates` — merged params come back in one object:**
```tsx
const [coords, setCoords] = useQueryStates({
lat: parseAsFloat,
lng: parseAsFloat
})
const shareLocation = async () => {
const search = await setCoords({ lat: 48.8566, lng: 2.3522 })
// search.toString(): "lat=48.8566&lng=2.3522"
}
```
**When NOT to use this pattern:**
- You only need the local in-memory value — read it from the returned state, not the setter Promise.
- You are inside Server Components — use `createSerializer` instead (see `perf-serialize-utility`).
Reference: [nuqs Batching](https://nuqs.dev/docs/batching)
references/state-standard-schema.md
---
title: Use Standard Schema for Cross-Library Validation
impact: MEDIUM
impactDescription: one parser map validates nuqs, tRPC, route validators, and forms — no duplicated schema
tags: state, standard-schema, zod, valibot, trpc, validation
---
## Use Standard Schema for Cross-Library Validation
Since nuqs v2.5, every `parseAsX` builder implements the [Standard Schema](https://standardschema.dev) interface, and `parseAsJson` accepts any Standard Schema validator (Zod, Valibot, ArkType, Effect Schema, …) directly in the validator slot. That means one parser map can drive **all of**: client-side `useQueryState`, server-side `createSearchParamsCache`/`createLoader`, tRPC procedure inputs, TanStack Router search-param validation, and form-level validation — without redefining the schema in three places.
**Incorrect (shape defined in three places, drift inevitable):**
```ts
// lib/searchParams.ts
import { parseAsString, parseAsInteger } from 'nuqs'
export const searchParams = {
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1)
}
// server/trpc/search.ts
import { z } from 'zod'
export const searchInput = z.object({
q: z.string().default(''),
page: z.number().int().default(1) // Default '1' here, '0' somewhere else — bug waiting to happen
})
// server/route-validator.ts
export function validateSearch(v: unknown) {
// Hand-rolled — drifts from both of the above
}
```
**Correct (one nuqs parser map, consumed everywhere via Standard Schema):**
```ts
// lib/searchParams.ts
import { parseAsString, parseAsInteger, createStandardSchemaV1 } from 'nuqs'
export const searchParamsMap = {
q: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1)
}
// Expose the same map as a Standard Schema for any v1-compatible consumer
export const searchParamsSchema = createStandardSchemaV1(searchParamsMap)
```
```ts
// server/trpc/search.ts — tRPC v11 accepts Standard Schemas
import { publicProcedure } from '@/server/trpc'
import { searchParamsSchema } from '@/lib/searchParams'
export const search = publicProcedure
.input(searchParamsSchema)
.query(({ input }) => {
// input is { q: string; page: number }
return runSearch(input.q, input.page)
})
```
```tsx
// app/search/page.tsx (client) — same map
'use client'
import { useQueryStates } from 'nuqs'
import { searchParamsMap } from '@/lib/searchParams'
export default function Filters() {
const [{ q, page }, setSearch] = useQueryStates(searchParamsMap)
return <SearchUI q={q} page={page} onChange={setSearch} />
}
```
**Using a Standard Schema library inside `parseAsJson`:**
`parseAsJson` accepts any Standard Schema validator directly — Zod 4+, Valibot 0.30+, ArkType, and Effect Schema all qualify.
```tsx
import { z } from 'zod'
import { parseAsJson, useQueryState } from 'nuqs'
const FiltersSchema = z.object({
minPrice: z.number(),
categories: z.array(z.string())
})
const [filters] = useQueryState(
'filters',
parseAsJson(FiltersSchema).withDefault({ minPrice: 0, categories: [] })
)
// Invalid JSON → null → falls back to the default
```
**When NOT to use this pattern:**
- The shape only lives in one file and one consumer — keep it simple with plain parsers; the Standard Schema indirection adds zero value.
- You're on nuqs < 2.5 — `createStandardSchemaV1` doesn't exist there. Either upgrade or write the bridge by hand.
Reference: [nuqs 2.5 release notes](https://nuqs.dev/blog/nuqs-2.5)
references/state-use-query-states.md
---
title: Use useQueryStates for Related Parameters
impact: HIGH
impactDescription: gives a single typed object and one combined URLSearchParams flush
tags: state, useQueryStates, batching, atomic, related-params
---
## Use useQueryStates for Related Parameters
When multiple URL parameters are logically related (coordinates, date ranges, filter sets), prefer `useQueryStates` over a tower of `useQueryState` calls. nuqs already batches sibling setter calls within the same event-loop tick into a single URL flush, so the win isn't "fewer history entries" — it's a single typed state object, a single combined update payload, and a single returned `URLSearchParams` you can inspect for the merged result.
**Incorrect (one hook per parameter):**
```tsx
'use client'
import { useQueryState, parseAsFloat, parseAsInteger } from 'nuqs'
export default function MapView() {
const [lat, setLat] = useQueryState('lat', parseAsFloat.withDefault(0))
const [lng, setLng] = useQueryState('lng', parseAsFloat.withDefault(0))
const [zoom, setZoom] = useQueryState('zoom', parseAsInteger.withDefault(10))
const goToParis = () => {
setLat(48.8566)
setLng(2.3522)
setZoom(12)
// Three setters, three places to keep in sync.
// Each setter resolves to its own URLSearchParams Promise — no single object holding the merged result.
}
return <button onClick={goToParis}>Go to Paris</button>
}
```
**Correct (one hook, one atomic update):**
```tsx
'use client'
import { useQueryStates, parseAsFloat, parseAsInteger } from 'nuqs'
export default function MapView() {
const [coords, setCoords] = useQueryStates({
lat: parseAsFloat.withDefault(0),
lng: parseAsFloat.withDefault(0),
zoom: parseAsInteger.withDefault(10)
})
const goToParis = async () => {
const search = await setCoords({ lat: 48.8566, lng: 2.3522, zoom: 12 })
// search: URLSearchParams with all three keys merged
}
return (
<div>
<p>Location: {coords.lat}, {coords.lng} (zoom: {coords.zoom})</p>
<button onClick={goToParis}>Go to Paris</button>
</div>
)
}
```
**Partial updates and clearing:**
```tsx
setCoords({ zoom: 15 }) // Update only zoom; lat/lng untouched
setCoords({ lat: 51.5074, lng: -0.1278 }) // Update lat/lng; keep zoom
setCoords(null) // Clear every key in the object
```
**When NOT to use this pattern:**
- Parameters belong to unrelated UI surfaces (e.g., a sidebar filter vs. an unrelated pagination control) — coupling them in one hook causes unnecessary re-renders of components that only need one key.
- Non-Next.js adapters (v2.5+) automatically scope re-renders to specific keys when you use `useQueryState`, so splitting into independent hooks can actually be faster for re-render-sensitive trees. See `perf-avoid-rerender`.
Reference: [nuqs Batching](https://nuqs.dev/docs/batching)
SKILL.md
---
name: nuqs
description: nuqs (type-safe URL query state) best practices for Next.js and other React frameworks. This skill should be used when writing, reviewing, or refactoring code that uses nuqs for URL state management. Triggers on tasks involving useQueryState, useQueryStates, search params, URL state, query parameters, nuqs parsers, limitUrlUpdates, Standard Schema, NuqsAdapter, or Next.js routing with state.
---
# Community nuqs Best Practices for Next.js & React
Comprehensive guide for type-safe URL query state management with nuqs across Next.js, React Router, TanStack Router, Remix, and plain React. Covers nuqs v2.5–v2.9 features. Contains 39 rules across 8 categories, prioritized by impact to guide code generation, refactoring, and code review.
## When to Apply
Reference these guidelines when:
- Implementing URL-based state with nuqs
- Setting up nuqs in a Next.js or React Router project
- Configuring parsers for URL parameters
- Integrating URL state with Server Components
- Optimizing URL update performance (`limitUrlUpdates`, key isolation)
- Sharing parser definitions with tRPC / TanStack Router / forms via Standard Schema
- Debugging nuqs-related issues
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Parser Configuration | CRITICAL | `parser-` |
| 2 | Adapter & Setup | CRITICAL | `setup-` |
| 3 | State Management | HIGH | `state-` |
| 4 | Server Integration | HIGH | `server-` |
| 5 | Performance Optimization | MEDIUM | `perf-` |
| 6 | History & Navigation | MEDIUM | `history-` |
| 7 | Debugging & Testing | LOW-MEDIUM | `debug-` |
| 8 | Advanced Patterns | LOW | `advanced-` |
## Quick Reference
### 1. Parser Configuration (CRITICAL)
- [`parser-use-typed-parsers`](references/parser-use-typed-parsers.md) — Use typed parsers for non-string values
- [`parser-with-default`](references/parser-with-default.md) — Use withDefault for non-nullable state
- [`parser-enum-validation`](references/parser-enum-validation.md) — Use enum parsers for constrained values
- [`parser-array-format`](references/parser-array-format.md) — Choose correct array parser format
- [`parser-json-validation`](references/parser-json-validation.md) — Validate JSON parser input
- [`parser-date-format`](references/parser-date-format.md) — Select appropriate date parser
- [`parser-index-offset`](references/parser-index-offset.md) — Use parseAsIndex for 1-based URL display
### 2. Adapter & Setup (CRITICAL)
- [`setup-nuqs-adapter`](references/setup-nuqs-adapter.md) — Wrap app with NuqsAdapter
- [`setup-use-client`](references/setup-use-client.md) — Add 'use client' directive for hooks
- [`setup-import-server`](references/setup-import-server.md) — Import server utilities from nuqs/server
- [`setup-nextjs-version`](references/setup-nextjs-version.md) — Ensure compatible Next.js version
- [`setup-shared-parsers`](references/setup-shared-parsers.md) — Define shared parsers in dedicated file
- [`setup-default-options`](references/setup-default-options.md) — Configure app-wide defaults on NuqsAdapter (v2.5+)
### 3. State Management (HIGH)
- [`state-use-query-states`](references/state-use-query-states.md) — Use useQueryStates for related parameters
- [`state-clear-with-null`](references/state-clear-with-null.md) — Clear URL parameters with null
- [`state-avoid-derived`](references/state-avoid-derived.md) — Avoid derived state from URL parameters
- [`state-options-inheritance`](references/state-options-inheritance.md) — Use withOptions for parser-level configuration
- [`state-setter-return`](references/state-setter-return.md) — Use setter return value for URL access
- [`state-standard-schema`](references/state-standard-schema.md) — Use Standard Schema for cross-library validation (v2.5+)
### 4. Server Integration (HIGH)
- [`server-search-params-cache`](references/server-search-params-cache.md) — Use createSearchParamsCache (or createLoader) for Server Components
- [`server-shallow-false`](references/server-shallow-false.md) — Use shallow:false to trigger server re-renders
- [`server-use-transition`](references/server-use-transition.md) — Integrate useTransition for loading states
- [`server-parse-before-get`](references/server-parse-before-get.md) — Call parse() before get() in Server Components
- [`server-next15-async`](references/server-next15-async.md) — Handle async searchParams in Next.js 15+
### 5. Performance Optimization (MEDIUM)
- [`perf-throttle-updates`](references/perf-throttle-updates.md) — Throttle rapid URL updates with `limitUrlUpdates`
- [`perf-debounce-search`](references/perf-debounce-search.md) — Debounce search input with built-in `limitUrlUpdates`
- [`perf-clear-on-default`](references/perf-clear-on-default.md) — Use clearOnDefault for clean URLs
- [`perf-avoid-rerender`](references/perf-avoid-rerender.md) — Memoize components using URL state (Next.js)
- [`perf-key-isolation`](references/perf-key-isolation.md) — Rely on key isolation outside Next.js (v2.5+)
- [`perf-serialize-utility`](references/perf-serialize-utility.md) — Use createSerializer for link URLs
### 6. History & Navigation (MEDIUM)
- [`history-push-navigation`](references/history-push-navigation.md) — Choose history:push vs history:replace
- [`history-scroll-behavior`](references/history-scroll-behavior.md) — Control scroll behavior on URL changes
### 7. Debugging & Testing (LOW-MEDIUM)
- [`debug-enable-logging`](references/debug-enable-logging.md) — Enable debug logging for troubleshooting
- [`debug-testing`](references/debug-testing.md) — Test components with URL state
### 8. Advanced Patterns (LOW)
- [`advanced-custom-parsers`](references/advanced-custom-parsers.md) — Create custom parsers for complex types
- [`advanced-url-keys`](references/advanced-url-keys.md) — Use urlKeys for shorter URLs
- [`advanced-eq-function`](references/advanced-eq-function.md) — Implement eq function for object parsers
- [`advanced-framework-adapters`](references/advanced-framework-adapters.md) — Use framework-specific adapters
- [`advanced-process-url-search-params`](references/advanced-process-url-search-params.md) — Canonicalize URL shape with `processUrlSearchParams` (v2.6+)
## How to Use
Read individual reference files for detailed explanations and code examples:
- [Section definitions](references/_sections.md) — Category structure and impact levels
- [Rule template](assets/templates/_template.md) — Template for adding new rules
## Reference Files
| File | Description |
|------|-------------|
| [AGENTS.md](AGENTS.md) | Complete compiled guide with all rules |
| [references/_sections.md](references/_sections.md) | Category definitions and ordering |
| [assets/templates/_template.md](assets/templates/_template.md) | Template for new rules |
| [metadata.json](metadata.json) | Version and reference information |