nuqs-presets
v0.1.3
Published
High-level pattern hooks for nuqs - pagination, filtering, sorting, and more
Maintainers
Readme
nuqs-presets
High-level pattern hooks for nuqs - Stop reinventing pagination, filtering, sorting, and search.
Why?
nuqs is an excellent library for managing URL state, but building common patterns like pagination, filtering, and sorting still requires boilerplate. nuqs-presets provides ready-to-use hooks that solve these patterns with best practices baked in.
Before
// 30+ lines of boilerplate for pagination alone
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
const [pageSize, setPageSize] = useQueryState('pageSize', parseAsInteger.withDefault(10))
// ... handle navigation, validation, edge cases ...After
// 2 lines, everything handled
const { page, pageSize, nextPage, prevPage, hasNextPage } = usePagination()Features
- ✅ 7 production-ready hooks - Pagination, filtering, sorting, search, tabs, date ranges, multi-select
- ✅ Type-safe - Full TypeScript support with excellent inference
- ✅ Zero config - Sensible defaults for immediate use
- ✅ Customizable - Override any behavior when needed
- ✅ Tiny - Tree-shakeable, minimal bundle size
- ✅ Framework agnostic - Works anywhere nuqs works (Next.js, Remix, React Router, etc.)
Installation
npm install nuqs-presets nuqs
# or
pnpm add nuqs-presets nuqs
# or
yarn add nuqs-presets nuqs
# or
bun add nuqs-presets nuqsRequirements:
nuqs^2.0.0react^18.3.0 or ^19.0.0
Quick Start
1. Set up nuqs adapter
Follow the nuqs setup guide for your framework:
// Next.js App Router - app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app'
export default function RootLayout({ children }) {
return (
<html>
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
)
}2. Use presets in your components
'use client'
import { usePagination, useFilters, useSorting } from 'nuqs-presets'
import { parseAsString, parseAsFloat } from 'nuqs'
const filterParsers = {
category: parseAsString,
minPrice: parseAsFloat,
}
export function ProductList() {
const { page, pageSize, nextPage, prevPage, hasNextPage, hasPrevPage } = usePagination()
const { filters, setFilter, clearFilters } = useFilters({
parsers: filterParsers,
})
const { sortBy, sortOrder, toggleSort } = useSorting({
columns: ['name', 'price', 'date'] as const
})
return (
<div>
{/* Your UI */}
<button onClick={prevPage} disabled={!hasPrevPage}>Previous</button>
<span>Page {page}</span>
<button onClick={nextPage} disabled={!hasNextPage}>Next</button>
</div>
)
}Hooks
usePagination
Complete pagination with all the bells and whistles.
const {
page, // Current page (1-indexed)
pageSize, // Items per page
totalPages, // Total pages (computed)
hasNextPage, // Can go forward
hasPrevPage, // Can go back
nextPage, // Go to next page
prevPage, // Go to previous page
goToPage, // Go to specific page
setPageSize, // Change page size
} = usePagination({
defaultPageSize: 10,
totalItems: 1000,
})useFilters
Type-safe filter management with nuqs parsers.
import { parseAsString, parseAsFloat } from 'nuqs'
const filterParsers = {
category: parseAsString,
minPrice: parseAsFloat,
maxPrice: parseAsFloat,
inStock: parseAsBoolean,
}
const {
filters, // Current filters (type-safe)
setFilter, // Set a single filter
clearFilters, // Clear all filters
hasFilters, // Any filters active?
} = useFilters({
parsers: filterParsers,
})
// filters.category is string | null
// filters.minPrice is number | nulluseSorting
Smart column sorting with toggle behavior.
const {
sortBy, // Current sort column
sortOrder, // 'asc' | 'desc' | null
toggleSort, // Toggle column (null → asc → desc → null)
isSortedBy, // Check if column is sorted
} = useSorting({
columns: ['name', 'date', 'price'] as const
})useSearch
Debounced search with min length validation.
const {
query, // Current search query
debouncedQuery, // Debounced value for API calls
setQuery, // Update search
isDebouncing, // Debounce in progress
} = useSearch({
debounce: 300,
minLength: 2,
})useTabs
Type-safe tab navigation.
const {
activeTab, // Current tab (type-safe)
setTab, // Change tab
isActive, // Check if tab is active
} = useTabs(['overview', 'analytics', 'settings'] as const)useDateRange
Date range selection with presets.
const {
startDate, // Start date
endDate, // End date
setRange, // Set both dates
presets, // Quick presets (last 7 days, etc.)
} = useDateRange({
defaultPreset: 'last7days'
})useMultiSelect
Array-based multi-selection.
const {
selected, // Selected items
toggle, // Toggle selection
selectAll, // Select all
deselectAll, // Deselect all
} = useMultiSelect({
allItems: ['item1', 'item2', 'item3']
})API Reference
For detailed API documentation for each hook, see the source code or TypeScript definitions. All hooks are fully typed with JSDoc comments.
Live Examples
This repository includes working example applications in the examples/ directory. These are complete, runnable apps that demonstrate real-world usage.
🚀 Available Examples
nextjs-basic - ✅ Complete
A simple Next.js 16 app demonstrating basic usage with:
- Pagination with page size control
- Debounced search
- Multi-column sorting
- Clean, modern UI with dark mode support
Run it:
cd examples/nextjs-basic
npm run devnextjs-ecommerce - 🧪 Beta
Advanced e-commerce filtering interface with:
- Multi-faceted filtering (category, price range, brand)
- Filter badges with clear functionality
- Type-safe parsers with nuqs
Run it:
cd examples/nextjs-ecommerce
npm run devnextjs-dashboard - 🧪 Beta
Admin dashboard demonstrating:
- Tab-based navigation
- Data tables with all features
- Date range filtering
Run it:
cd examples/nextjs-dashboard
npm run devreact-vite - 📦 Coming Soon
Framework-agnostic React SPA with:
- Vite for fast development
- React Router integration
- Client-side routing
See the examples README for more details.
Code Examples
E-commerce Product List
'use client'
import { usePagination, useFilters, useSorting, useSearch } from 'nuqs-presets'
import { parseAsString, parseAsFloat, parseAsBoolean } from 'nuqs'
const filterParsers = {
category: parseAsString,
minPrice: parseAsFloat,
maxPrice: parseAsFloat,
inStock: parseAsBoolean,
}
export function ProductList() {
const { page, pageSize, setPage, hasNextPage, hasPrevPage } = usePagination({
defaultPageSize: 24,
totalItems: 1000,
})
const { filters, setFilter, clearFilters, hasFilters } = useFilters({
parsers: filterParsers,
})
const { sortBy, sortOrder, toggleSort } = useSorting({
columns: ['name', 'price', 'rating'] as const,
defaultColumn: 'name',
defaultOrder: 'asc',
})
const { query, debouncedQuery, setQuery } = useSearch({
debounce: 300,
minLength: 2,
})
// Fetch products with all filters
const { data: products } = useProducts({
page,
pageSize,
...filters,
sortBy,
sortOrder,
search: debouncedQuery,
})
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
<div>
<select onChange={(e) => setFilter('category', e.target.value)}>
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="books">Books</option>
<option value="clothing">Clothing</option>
</select>
<button onClick={clearFilters} disabled={!hasFilters}>
Clear Filters
</button>
</div>
<div>
<button onClick={() => toggleSort('name')}>
Name {sortBy === 'name' && (sortOrder === 'asc' ? '↑' : '↓')}
</button>
<button onClick={() => toggleSort('price')}>
Price {sortBy === 'price' && (sortOrder === 'asc' ? '↑' : '↓')}
</button>
</div>
<div>
{products?.map((product) => (
<div key={product.id}>{product.name}</div>
))}
</div>
<div>
<button onClick={() => setPage(page - 1)} disabled={!hasPrevPage}>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => setPage(page + 1)} disabled={!hasNextPage}>
Next
</button>
</div>
</div>
)
}Tree-shaking
All hooks are tree-shakeable. Import only what you need:
// Import individual hooks
import { usePagination } from 'nuqs-presets/pagination'
import { useFilters } from 'nuqs-presets/filtering'
import { useSorting } from 'nuqs-presets/sorting'TypeScript
All hooks are fully typed with excellent type inference. No need to manually specify types in most cases:
const { activeTab } = useTabs(['overview', 'analytics', 'settings'] as const)
// activeTab is typed as 'overview' | 'analytics' | 'settings'
const filterParsers = {
category: parseAsString,
minPrice: parseAsFloat,
}
const { filters } = useFilters({
parsers: filterParsers,
})
// filters.category is typed as string | null
// filters.minPrice is typed as number | nullContributing
Contributions are welcome! Please read our Contributing Guide first.
License
MIT © Hitesh Agrawal
Credits
Built on top of the excellent nuqs by François Best.
⭐ If you find this useful, please star the repo!
