Nuqs — Query/Search Params State Manager for Next.js
Overview — What is Nuqs?
Nuqs is a lightweight state manager that stores your React state inside the URL search params—in a typed, parsed, and reactive way.
It integrates perfectly with:
- Next.js App Router
- React Server Components
- React Client Components
Nuqs gives you:
- Easy syncing between UI state ↔ URL query state
- Full type safety using parsers
- Lightweight state storage without global stores
- Persisted state between refreshes and sharable URLs
Why Use Query/Search Params as State? Why Use Nuqs?
Why store state in the URL?
Storing state in search params is useful for:
- Filters (category=books&page=3)
- Sort options (sort=date)
- Pagination
- Complex search
- Ability to share links with filters/search/pagination embedded
Problems using useSearchParams or manual URL APIs
- Values are always strings
- Sometimes we need to set default value in order to delete query key from URL when value becomes “Empty String”
- We need to set Custom Parsers for commplex or composite types
Why Nuqs?
Nuqs provides:
- Typed, validated search params
- Automatic parsing & stringification
- Automatic history.push / replace handling
- React state synced with URL
- Support for multiple parameters
- SSR support via nuqs-server
Understanding Nuqs Parsers
Parsers convert URL string values into typed values.
Examples:
import { booleanParser, numberParser, stringParser, jsonParser } from 'nuqs';Common built-in parsers:
| Parser | Input Example | Output |
|---|---|---|
stringParser | ?q=hello | "hello" |
numberParser | ?page=3 | 3 (number) |
booleanParser | ?enabled=true | true |
jsonParser | ?filters=["a","b"] | ["a","b"] |
Creating a Custom Parser
A custom parser allows typed values beyond the built-ins.
Example: parser for comma-separated arrays:
import { createParser } from 'nuqs';
export const arrayParser = createParser<string[]>({
parse: (value) => value.split(','),
serialize: (arr) => arr.join(',')
});Usage:
const [tags, setTags] = useQueryState('tags', arrayParser);Example: custom parser for date-range (from - to):
export const dateRangeParser = createParser({
parse(queryVal) {
return {
from: queryVal.split('/')[0],
to: queryVal.split('/')[1]
}
},
serialize(value: { from?: Date | string; to?: Date | string }) {
if (!value?.from || !value?.to) return ''
// Convert Date objects to ISO strings if needed
// current base-date-range component return object (from, to) and they are both in Date type
const fromString =
value.from instanceof Date
? formatDate(value.from)
: value.from
const toString =
value.to instanceof Date
? formatDate(value.to)
: value.to
return `${fromString}/${toString}`
}
})Using useQueryState — Manage One Search Param
Basic usage:
import { useQueryState, numberParser } from 'nuqs';
const [page, setPage] = useQueryState('page', numberParser.withDefault(1));Options Available
useQueryState('page', numberParser, {
history: 'push' | 'replace',
shallow: true | false,
})| Option | Meaning |
|---|---|
| history | Whether updates push new history entries or replace the current one |
| shallow | Avoids full Next.js re-render/navigation |
Using useQueryStates — Manage Multiple Params
Useful when many filters are related.
import { useQueryStates, numberParser, stringParser } from 'nuqs';
const [filters, setFilters] = useQueryStates({
page: numberParser.withDefault(1),
search: stringParser.withDefault(""),
sort: stringParser
});Updating:
setFilters({ page: 2, sort: "date" });This updates all params in one synchronized action.
When to use it?
- Complex filter systems
- Admin dashboards
- Tables (page, sort, search, perPage)
- Multi-value search forms
Do We Always Need to Connect Query Params with Actual Values?
Short answer: No.
While syncing filters, search, and pagination with URL query params is a powerful pattern, it’s not always necessary. The decision depends on your use case and user experience requirements.
The Problem We Solved
Previously, we managed filters using local state (e.g., React state or a store) without syncing to query params. However, we encountered a significant UX issue:
Scenario: A user opens a filter popover, adjusts multiple filters, and then closes the popover without applying the changes. The filters remain in local state, but the URL doesn’t reflect them. When the user navigates away or refreshes the page, they lose track of what filters were applied. More critically, if they close the filter popover, they might not recognize why the displayed data is filtered differently than expected.
Solution: By syncing filter state with query params, we:
- Notify users of active filters through the URL
- Use the URL as the source of truth for filters, search, and pagination
- Enable shareable links with filters embedded
- Provide clear visual feedback when filters are active
When Query Params Are Not Necessary
In simple scenarios, local state management is perfectly fine:
- Simple, single-use filters that don’t need to be shared
- Temporary UI state (e.g., dropdown open/closed)
- Non-critical filters that users don’t need to bookmark
- Internal admin tools where URL sharing isn’t important
Example: A simple “Show archived items” toggle that doesn’t need to persist across page refreshes or be shareable.
Best Practice: Use Query Params When:
- Filters affect displayed data significantly
- Users might want to bookmark or share filtered views
- You need to persist state across page refreshes
- Multiple filters interact with each other
- You want to provide clear feedback about active filters
Visual Feedback for Applied Filters
Regardless of whether you use query params or local state, providing visual feedback when filters are applied is crucial for good UX.
Why Visual Feedback Matters
Users need to know when filters are active, especially when:
- They’ve applied filters and closed the filter panel
- They’ve navigated to a page with filters already in the URL
- Multiple filters are active simultaneously
- They want to quickly identify and clear active filters
Implementation Approaches
1. Visual Indicators
Common UI patterns for indicating active filters:
Benefits of Visual Feedback
- Improved UX: Users immediately understand when filters are affecting their view
- Reduced confusion: Clear indication of why data might be filtered
- Better discoverability: Users can easily identify and manage active filters
- Accessibility: Visual indicators help all users understand the current state
Loading Parsed Params on the Server (nuqs-server)
Nuqs supports SSR via the nuqs-server package.
Example:
// app/products/page.tsx
import { parseSearchParams, numberParser, stringParser } from 'nuqs-server';
export default async function Page({ searchParams }) {
const params = parseSearchParams(searchParams, {
page: numberParser.withDefault(1),
q: stringParser.withDefault("")
});
const page = params.page;
const q = params.q;
const products = await getProducts({ page, q });
return <ProductsPage products={products} page={page} q={q} />
}This ensures your server logic uses typed search params instead of raw strings.
References
For the most up-to-date and detailed information, please refer to the official documentation:
Summary
- Nuqs is a powerful state manager that stores UI state inside the URL.
- It provides typed parsers, solving the limitations of string-only search params.
useQueryStateis ideal for managing a single parameter.useQueryStatesis ideal for dashboards or multi-filter pages.- Nuqs integrates with server components using
nuqs-server. - It helps build applications where state is shareable, persisted, and bookmarkable.