nuqs
URL search state management with nuqs in DeesseJS
nuqs
nuqs is included in DeesseJS for type-safe URL search state management, enabling you to manage filters, pagination, and search parameters in the URL.
Setup
nuqs comes pre-configured in DeesseJS.
Basic Usage
Reading and Writing Query Strings
'use client'
import { useQueryState } from 'nuqs'
export default function SearchPage() {
const [search, setSearch] = useQueryState('search')
return (
<div>
<input
type="text"
value={search || ''}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
<p>Searching for: {search || 'everything'}</p>
</div>
)
}Default Values
const [page, setPage] = useQueryState('page', {
defaultValue: 1,
})With Parser for Type Safety
const [page, setPage] = useQueryState('page', {
defaultValue: 1,
parse: (value) => parseInt(value),
serialize: (value) => value.toString(),
})Common Patterns
Search and Filter
'use client'
import { useQueryState } from 'nuqs'
export function ProductFilters() {
const [search, setSearch] = useQueryState('search')
const [category, setCategory] = useQueryState('category')
const [minPrice, setMinPrice] = useQueryState('minPrice', {
parse: (value) => parseInt(value) || 0,
serialize: (value) => value.toString(),
})
return (
<div>
<input
type="text"
value={search || ''}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search products..."
/>
<select
value={category || ''}
onChange={(e) => setCategory(e.target.value || null)}
>
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<input
type="number"
value={minPrice || 0}
onChange={(e) => setMinPrice(parseInt(e.target.value) || 0)}
placeholder="Min price"
/>
</div>
)
}Pagination
'use client'
import { useQueryState } from 'nuqs'
export function Pagination({ totalPages }: { totalPages: number }) {
const [page, setPage] = useQueryState('page', {
defaultValue: 1,
parse: (value) => {
const parsed = parseInt(value)
return isNaN(parsed) ? 1 : parsed
},
})
return (
<div>
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page <= 1}
>
Previous
</button>
<span>Page {page} of {totalPages}</span>
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page >= totalPages}
>
Next
</button>
</div>
)
}Sorting
'use client'
import { useQueryState } from 'nuqs'
type SortOption = 'name' | 'date' | 'price'
export function SortControl() {
const [sortBy, setSortBy] = useQueryState('sort', {
defaultValue: 'date' as SortOption,
parse: (value): SortOption => {
if (value === 'name' || value === 'date' || value === 'price') {
return value
}
return 'date'
},
})
const [order, setOrder] = useQueryState('order', {
defaultValue: 'desc' as 'asc' | 'desc',
parse: (value): 'asc' | 'desc' =>
value === 'asc' ? 'asc' : 'desc',
})
return (
<div>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value as SortOption)}>
<option value="name">Name</option>
<option value="date">Date</option>
<option value="price">Price</option>
</select>
<button onClick={() => setOrder(order === 'asc' ? 'desc' : 'asc')}>
{order === 'asc' ? '↑' : '↓'}
</button>
</div>
)
}Advanced Usage
Multiple Values
'use client'
import { useQueryStates } from 'nuqs'
export function Filters() {
const [filters, setFilters] = useQueryStates({
search: { defaultValue: '' },
category: { defaultValue: '' },
minPrice: {
defaultValue: 0,
parse: (value) => parseInt(value) || 0,
},
})
return (
<div>
<input
value={filters.search}
onChange={(e) => setFilters({ search: e.target.value })}
/>
<select
value={filters.category}
onChange={(e) => setFilters({ category: e.target.value })}
>
<option value="">All</option>
<option value="electronics">Electronics</option>
</select>
</div>
)
}Array Values
'use client'
import { useQueryState } from 'nuqs'
export function TagFilter() {
const [tags, setTags] = useQueryState('tags', {
defaultValue: [] as string[],
parse: (value) => value.split(','),
serialize: (values) => values.join(','),
})
const toggleTag = (tag: string) => {
setTags(tags.includes(tag)
? tags.filter((t) => t !== tag)
: [...tags, tag]
)
}
return (
<div>
{['javascript', 'typescript', 'react'].map((tag) => (
<button
key={tag}
onClick={() => toggleTag(tag)}
style={{
background: tags.includes(tag) ? 'blue' : 'gray',
}}
>
{tag}
</button>
))}
</div>
)
}Clear State
const [search, setSearch] = useQueryState('search')
// Clear by setting to null
setSearch(null)
// Or use the clear method
setSearch(null, { history: 'push' })Server-Side Rendering
Reading Query Params in Server Components
// app/posts/page.tsx
import { searchParams } from 'nuqs/server'
export default async function PostsPage({
searchParams,
}: {
searchParams: { search?: string; category?: string }
}) {
const { search, category } = await searchParams
const posts = await db.posts.findMany({
where: {
...(search && {
title: { contains: search },
}),
...(category && {
category,
}),
},
})
return (
<div>
<PostsFilters />
<PostsList posts={posts} />
</div>
)
}Type-Safe Server Params
import { parseServerSearchParams } from 'nuqs/server'
interface PostFilters {
search: string
category: string
page: number
}
export default async function PostsPage({
searchParams,
}: {
searchParams: Record<string, string | string[] | undefined>
}) {
const filters = await parseServerSearchParams<PostFilters>(searchParams, {
search: { defaultValue: '' },
category: { defaultValue: '' },
page: {
defaultValue: 1,
parse: (value) => parseInt(value),
},
})
const posts = await db.posts.findMany({
where: {
...(filters.search && {
title: { contains: filters.search },
}),
},
skip: (filters.page - 1) * 10,
take: 10,
})
return <PostsList posts={posts} />
}Shallow Routing
Update URL Without Page Navigation
const [search, setSearch] = useQueryState('search', {
// Update URL but don't trigger page navigation
shallow: false, // Default - updates URL and navigates
// or
shallow: true, // Only updates URL, no navigation
})History Management
Push vs Replace
const [search, setSearch] = useQueryState('search')
// Push new state (adds to history) - default
setSearch('query')
// Replace current state (doesn't add to history)
setSearch('query', { history: 'replace' })
// Push state (explicit)
setSearch('query', { history: 'push' })Debouncing
Debounce Search Input
'use client'
import { useQueryState } from 'nuqs'
import { useEffect, useState } from 'react'
export function DebouncedSearch() {
const [search, setSearch] = useQueryState('search')
const [localValue, setLocalValue] = useState(search || '')
useEffect(() => {
const timer = setTimeout(() => {
setSearch(localValue)
}, 500)
return () => clearTimeout(timer)
}, [localValue, setSearch])
return (
<input
type="text"
value={localValue}
onChange={(e) => setLocalValue(e.target.value)}
placeholder="Search..."
/>
)
}Syncing with Server State
Combine with TanStack Query
'use client'
import { useQueryState } from 'nuqs'
import { useQuery } from '@deessejs/query'
export function PostsPage() {
const [search, setSearch] = useQueryState('search', {
defaultValue: '',
})
const { data: posts } = useQuery({
queryKey: ['posts', search],
queryFn: () =>
fetch(`/api/posts?search=${search}`).then((res) => res.json()),
})
return (
<div>
<input
value={search || ''}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search posts..."
/>
<ul>
{posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}Preserving State
Keep Query Params on Navigation
'use client'
import Link from 'next/link'
import { useQueryState } from 'nuqs'
export function PostLink({ post }: { post: Post }) {
const [search] = useQueryState('search')
return (
<Link
href={`/posts/${post.id}?${new URLSearchParams({ search: search || '' })}`}
>
{post.title}
</Link>
)
}Best Practices
- Type Safety - Always use parsers for non-string values
- Default Values - Provide meaningful defaults for better UX
- Debouncing - Debounce search inputs to avoid excessive updates
- Null vs Empty - Use
nullto clear, empty string for valid empty state - Server-Side - Use
nuqs/serverfor reading params in Server Components
Common Use Cases
Faceted Search
'use client'
import { useQueryStates } from 'nuqs'
export function FacetedSearch() {
const [filters, setFilters] = useQueryStates({
q: { defaultValue: '' },
category: { defaultValue: '' },
brand: { defaultValue: '' },
priceMin: {
defaultValue: 0,
parse: (value) => parseInt(value) || 0,
},
priceMax: {
defaultValue: 1000,
parse: (value) => parseInt(value) || 1000,
},
})
const clearFilters = () => {
setFilters({
q: '',
category: '',
brand: '',
priceMin: 0,
priceMax: 1000,
})
}
return (
<div>
<input
value={filters.q}
onChange={(e) => setFilters({ q: e.target.value })}
placeholder="Search..."
/>
<select
value={filters.category}
onChange={(e) => setFilters({ category: e.target.value })}
>
{/* Categories */}
</select>
<button onClick={clearFilters}>Clear Filters</button>
</div>
)
}Next Steps
- Learn about Zustand
- Explore TanStack Query
- Return to State Management Overview