Type-safe search params state manager for Next.js - Like React.useState, but stored in the URL query string
Overall
score
96%
Server-side utilities for accessing and parsing query parameters in React Server Components, providing type-safe query parameter handling with request-scoped caching.
Creates a request-scoped cache for parsing and accessing query parameters in server components.
/**
* Creates a search params cache for server-side query parameter parsing
* @param parsers - Object mapping keys to their respective parsers
* @returns Cache instance with parse, get, and all methods
*/
function createSearchParamsCache<Parsers extends Record<string, ParserBuilder<any>>>(
parsers: Parsers
): SearchParamsCache<Parsers>;
interface SearchParamsCache<Parsers extends Record<string, ParserBuilder<any>>> {
/** Parse the incoming searchParams page prop using the parsers provided */
parse(searchParams: SearchParams): ParsedSearchParams<Parsers>;
parse(searchParams: Promise<SearchParams>): Promise<ParsedSearchParams<Parsers>>;
/** Get a single parsed value by key */
get<Key extends keyof Parsers>(key: Key): ParsedSearchParams<Parsers>[Key];
/** Get all parsed values */
all(): ParsedSearchParams<Parsers>;
}
type SearchParams = Record<string, string | string[] | undefined>;
type ParsedSearchParams<Parsers extends Record<string, ParserBuilder<any>>> = {
readonly [K in keyof Parsers]: inferParserType<Parsers[K]>;
};
type inferParserType<Input> =
Input extends ParserBuilder<any>
? Input extends ParserBuilder<any> & { defaultValue: infer Type }
? Type
: ReturnType<Input['parse']> | null
: never;Usage Examples:
// app/search/page.tsx
import { createSearchParamsCache, parseAsString, parseAsInteger, parseAsBoolean } from "nuqs/server";
// Define the search params schema
const searchParamsCache = createSearchParamsCache({
q: parseAsString,
page: parseAsInteger.withDefault(1),
category: parseAsString,
featured: parseAsBoolean.withDefault(false)
});
export default async function SearchPage({
searchParams
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
// Parse all query parameters (Next.js 15+ with Promise)
const { q, page, category, featured } = await searchParamsCache.parse(searchParams);
// Types are fully inferred:
// q: string | null
// page: number (always defined due to default)
// category: string | null
// featured: boolean (always defined due to default)
return (
<div>
<h1>Search Results</h1>
{q && <p>Searching for: {q}</p>}
<p>Page: {page}</p>
{category && <p>Category: {category}</p>}
<p>Featured only: {featured}</p>
</div>
);
}
// For Next.js 14 and earlier (synchronous searchParams)
export default function SearchPage({
searchParams
}: {
searchParams: { [key: string]: string | string[] | undefined }
}) {
const { q, page, category, featured } = searchParamsCache.parse(searchParams);
// ... rest of component
}Access individual parsed parameters without parsing the entire object.
/**
* Get a single parsed value by key
* @param key - The key to retrieve from the cache
* @throws Error if parse() hasn't been called first or key doesn't exist
*/
get<Key extends keyof Parsers>(key: Key): ParsedSearchParams<Parsers>[Key];
/**
* Get all parsed values from the cache
* @throws Error if parse() hasn't been called first
*/
all(): ParsedSearchParams<Parsers>;Usage Examples:
// In a server component after calling parse()
export default async function ProductPage({ searchParams }: { searchParams: Promise<any> }) {
// Parse all parameters first
await searchParamsCache.parse(searchParams);
// Access individual values in child components
return (
<div>
<SearchFilters />
<ProductList />
</div>
);
}
// Child server component can access individual values
function SearchFilters() {
const query = searchParamsCache.get('q');
const category = searchParamsCache.get('category');
return (
<div>
<p>Current search: {query}</p>
<p>Category: {category}</p>
</div>
);
}
// Or access all values
function DebugInfo() {
const allParams = searchParamsCache.all();
return <pre>{JSON.stringify(allParams, null, 2)}</pre>;
}Use the cache in generateMetadata functions for SEO and page metadata.
import { Metadata } from "next";
export async function generateMetadata({
searchParams
}: {
searchParams: Promise<any>
}): Promise<Metadata> {
const { q, category } = await searchParamsCache.parse(searchParams);
const title = q
? `Search results for "${q}"${category ? ` in ${category}` : ''}`
: 'Search';
return {
title,
description: `Find products${category ? ` in ${category}` : ''}`
};
}The cache works with all parser types and configurations.
import {
createSearchParamsCache,
parseAsStringEnum,
parseAsArrayOf,
parseAsJson
} from "nuqs/server";
// Complex parser configuration
const advancedCache = createSearchParamsCache({
// Enum with default
status: parseAsStringEnum(['draft', 'published', 'archived'] as const)
.withDefault('published'),
// Array of strings
tags: parseAsArrayOf(parseAsString),
// JSON object
filters: parseAsJson<{
minPrice: number;
maxPrice: number;
brand: string;
}>(),
// Date range
startDate: parseAsIsoDateTime,
endDate: parseAsIsoDateTime,
});
export default async function AdvancedSearchPage({
searchParams
}: {
searchParams: Promise<any>
}) {
const params = await advancedCache.parse(searchParams);
// All types are properly inferred:
// params.status: 'draft' | 'published' | 'archived'
// params.tags: string[] | null
// params.filters: { minPrice: number; maxPrice: number; brand: string } | null
// params.startDate: Date | null
// params.endDate: Date | null
}The cache is automatically scoped to each request using React's cache function.
// The same cache instance can be used across multiple server components
// in the same request without re-parsing
// layout.tsx
export default async function Layout({
searchParams
}: {
searchParams: Promise<any>
}) {
await searchParamsCache.parse(searchParams);
return (
<div>
<Header />
<Sidebar />
<main>{children}</main>
</div>
);
}
// components/Header.tsx
function Header() {
const query = searchParamsCache.get('q'); // Uses cached value
return <SearchInput defaultValue={query} />;
}
// components/Sidebar.tsx
function Sidebar() {
const category = searchParamsCache.get('category'); // Uses cached value
return <CategoryFilter selected={category} />;
}The cache includes built-in error handling for common scenarios.
// Error cases:
try {
// Throws if parse() hasn't been called first
const value = searchParamsCache.get('q');
} catch (error) {
console.error('Must call parse() before accessing values');
}
try {
// Throws if key doesn't exist in parser schema
const value = searchParamsCache.get('nonexistent');
} catch (error) {
console.error('Key not found in parser schema');
}
// Invalid query values are gracefully parsed as null
const params = await searchParamsCache.parse({
page: 'invalid-number', // Will be parsed as null
enabled: 'maybe' // Will be parsed as null (not 'true'/'false')
});Install with Tessl CLI
npx tessl i tessl/npm-nuqsevals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10