This guide will walk you through creating a simple search interface with nuqs. You’ll learn the core concepts while building something practical.
Make sure you’ve completed the Installation steps before continuing.
Let’s create a search component that stores the query in the URL.
Create your component
Create a new client component (for Next.js app router, mark it with 'use client'):'use client' // Only needed for Next.js App Router
import { useQueryState } from 'nuqs'
export default function Search() {
const [search, setSearch] = useQueryState('q')
return (
<div>
<input
type="text"
value={search || ''}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
<button onClick={() => setSearch(null)}>Clear</button>
{search && <p>Searching for: {search}</p>}
</div>
)
}
The useQueryState hook works just like useState, but syncs with the URL query string! Try it out
Add the component to your page and start typing. Watch the URL update in real-time:
- Type “react” → URL becomes
?q=react
- Click “Clear” → URL becomes
/ (query removed)
- Refresh the page → Your input persists!
The URL is the source of truth. When you reload the page, the state is restored from the URL automatically.
Adding Type Safety with Parsers
Let’s add pagination with proper number parsing:
Import parsers
nuqs provides built-in parsers for common types:components/Pagination.tsx
'use client'
import { useQueryState, parseAsInteger } from 'nuqs'
export default function Pagination() {
const [page, setPage] = useQueryState(
'page',
parseAsInteger.withDefault(1)
)
return (
<div>
<p>Current page: {page}</p>
<button
onClick={() => setPage(page - 1)}
disabled={page <= 1}
>
Previous
</button>
<button onClick={() => setPage(page + 1)}>
Next
</button>
<button onClick={() => setPage(1)}>
Reset to page 1
</button>
</div>
)
}
Setting page to 1 (the default value) will clear the query from the URL automatically.
Benefits of parsers
- Type safety:
page is always a number, never null
- Validation: Invalid values are rejected and fall back to the default
- Serialization: Numbers are properly converted to/from URL strings
Managing Multiple Query States
Use useQueryStates to manage related queries together:
'use client'
import { useQueryStates, parseAsInteger, parseAsStringLiteral } from 'nuqs'
const sortOrders = ['asc', 'desc'] as const
export default function Filter() {
const [filters, setFilters] = useQueryStates({
search: {
type: 'single',
parse: (value) => value,
serialize: (value) => value,
eq: (a, b) => a === b
},
page: parseAsInteger.withDefault(1),
sort: parseAsStringLiteral(sortOrders).withDefault('asc')
})
return (
<div>
<input
type="text"
value={filters.search || ''}
onChange={(e) => setFilters({
search: e.target.value,
page: 1 // Reset to page 1 when searching
})}
placeholder="Search..."
/>
<select
value={filters.sort}
onChange={(e) => setFilters({
sort: e.target.value as 'asc' | 'desc',
page: 1
})}
>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
<div>
<p>Page: {filters.page}</p>
<button onClick={() => setFilters({ page: filters.page - 1 })}>
Previous
</button>
<button onClick={() => setFilters({ page: filters.page + 1 })}>
Next
</button>
</div>
<button onClick={() => setFilters(null)}>
Reset all filters
</button>
</div>
)
}
Multiple state updates in the same event are automatically batched into a single URL update.
Next Steps
You now know the basics of nuqs! Here’s what to explore next:
Parsers
Learn about all built-in parsers and how to create custom ones
Options
Control history mode, shallow routing, and throttling
Server-Side
Use nuqs with Server Components and server-side rendering
Testing
Learn how to test components that use nuqs
Common Patterns
For search inputs, you might want to debounce URL updates:
import { useQueryState } from 'nuqs'
const [search, setSearch] = useQueryState('q', {
throttleMs: 500 // Wait 500ms before updating the URL
})
History Mode
By default, URL updates replace the current history entry. To enable Back button navigation:
import { useQueryState, parseAsInteger } from 'nuqs'
const [page, setPage] = useQueryState(
'page',
parseAsInteger.withDefault(1).withOptions({
history: 'push' // Append to history instead of replacing
})
)
Server Updates (Next.js)
By default, updates are client-only (shallow). To trigger server component re-renders:
import { useQueryState } from 'nuqs'
const [filter, setFilter] = useQueryState('filter', {
shallow: false // Notify server components of changes
})
Setting shallow: false will cause server components to re-render, which can be slower. Only use this when you need server-side updates.
Complete Example
Here’s a complete example combining everything:
'use client'
import { useQueryStates, parseAsInteger, parseAsString } from 'nuqs'
export default function ProductList() {
const [params, setParams] = useQueryStates({
search: parseAsString.withDefault(''),
page: parseAsInteger.withDefault(1),
limit: parseAsInteger.withDefault(10)
})
// In a real app, you'd fetch data based on these params
const { search, page, limit } = params
return (
<div>
<h1>Products</h1>
<input
type="text"
value={search}
onChange={(e) => setParams({
search: e.target.value,
page: 1
})}
placeholder="Search products..."
/>
<select
value={limit}
onChange={(e) => setParams({
limit: parseInt(e.target.value),
page: 1
})}
>
<option value="10">10 per page</option>
<option value="25">25 per page</option>
<option value="50">50 per page</option>
</select>
{/* Your product list here */}
<div className="results">
<p>Page {page}, showing {limit} items</p>
{search && <p>Searching for: {search}</p>}
</div>
<div className="pagination">
<button
onClick={() => setParams({ page: page - 1 })}
disabled={page <= 1}
>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => setParams({ page: page + 1 })}>
Next
</button>
</div>
<button onClick={() => setParams(null)}>
Clear all filters
</button>
</div>
)
}
This creates a URL like: ?search=laptop&page=2&limit=25