Type-safe search params state manager for Next.js - Like React.useState, but stored in the URL query string
Overall
score
96%
Comprehensive parser system for converting between URL query string values and typed JavaScript/TypeScript values, with built-in support for common data types and utilities for creating custom parsers.
Ready-to-use parsers for common data types with fluent configuration API.
/** String parser (default behavior) */
const parseAsString: ParserBuilder<string>;
/** Integer parser with NaN handling */
const parseAsInteger: ParserBuilder<number>;
/** Hexadecimal integer parser */
const parseAsHex: ParserBuilder<number>;
/** Float parser with NaN handling */
const parseAsFloat: ParserBuilder<number>;
/** Boolean parser (parses 'true'/'false' strings) */
const parseAsBoolean: ParserBuilder<boolean>;
/** Date parser from milliseconds since epoch */
const parseAsTimestamp: ParserBuilder<Date>;
/** Date parser from ISO-8601 strings */
const parseAsIsoDateTime: ParserBuilder<Date>;Usage Examples:
import {
parseAsString,
parseAsInteger,
parseAsBoolean,
parseAsTimestamp
} from "nuqs";
// Basic usage
const [name, setName] = useQueryState('name', parseAsString);
const [count, setCount] = useQueryState('count', parseAsInteger);
const [enabled, setEnabled] = useQueryState('enabled', parseAsBoolean);
// With default values
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
const [active, setActive] = useQueryState('active', parseAsBoolean.withDefault(false));
// Date handling
const [createdAt, setCreatedAt] = useQueryState('created', parseAsTimestamp);
const [updatedAt, setUpdatedAt] = useQueryState('updated', parseAsIsoDateTime);Type-safe parsers for restricted sets of values.
/**
* String-based enums provide better type-safety for known sets of values
* @param validValues - The enum values you want to accept
*/
function parseAsStringEnum<Enum extends string>(validValues: Enum[]): ParserBuilder<Enum>;
/**
* String-based literals provide better type-safety for known sets of values
* @param validValues - The readonly list of allowed values
*/
function parseAsStringLiteral<Literal extends string>(
validValues: readonly Literal[]
): ParserBuilder<Literal>;
/**
* Number-based literals provide better type-safety for known sets of values
* @param validValues - The readonly list of allowed values
*/
function parseAsNumberLiteral<Literal extends number>(
validValues: readonly Literal[]
): ParserBuilder<Literal>;Usage Examples:
// Enum parser
enum Status {
DRAFT = 'draft',
PUBLISHED = 'published',
ARCHIVED = 'archived'
}
const [status, setStatus] = useQueryState(
'status',
parseAsStringEnum(Object.values(Status)).withDefault(Status.DRAFT)
);
// String literal parser
const themes = ['light', 'dark', 'auto'] as const;
const [theme, setTheme] = useQueryState(
'theme',
parseAsStringLiteral(themes).withDefault('auto')
);
// Number literal parser
const ratings = [1, 2, 3, 4, 5] as const;
const [rating, setRating] = useQueryState(
'rating',
parseAsNumberLiteral(ratings)
);Parser for comma-separated arrays with configurable item parsers.
/**
* A comma-separated list of items
* @param itemParser - Parser for each individual item in the array
* @param separator - The character to use to separate items (default ',')
*/
function parseAsArrayOf<ItemType>(
itemParser: Parser<ItemType>,
separator?: string
): ParserBuilder<ItemType[]>;Usage Examples:
import { parseAsArrayOf, parseAsInteger, parseAsString } from "nuqs";
// Array of strings
const [tags, setTags] = useQueryState(
'tags',
parseAsArrayOf(parseAsString)
);
// Array of integers
const [ids, setIds] = useQueryState(
'ids',
parseAsArrayOf(parseAsInteger)
);
// Custom separator
const [categories, setCategories] = useQueryState(
'categories',
parseAsArrayOf(parseAsString, '|')
);
// Usage
setTags(['react', 'nextjs', 'typescript']);
setIds([1, 2, 3, 4]);
// URLs: ?tags=react,nextjs,typescript&ids=1,2,3,4Parser for complex objects serialized as JSON in the query string.
/**
* Encode any object shape into the querystring value as JSON
* Value is URI-encoded for safety, so it may not look nice in the URL
* @param parser - Optional parser (eg: Zod schema) to validate after JSON.parse
*/
function parseAsJson<T>(parser?: (value: unknown) => T): ParserBuilder<T>;Usage Examples:
import { parseAsJson } from "nuqs";
// Simple object
interface UserPrefs {
notifications: boolean;
theme: string;
language: string;
}
const [prefs, setPrefs] = useQueryState(
'prefs',
parseAsJson<UserPrefs>().withDefault({
notifications: true,
theme: 'light',
language: 'en'
})
);
// With validation using Zod
import { z } from 'zod';
const prefsSchema = z.object({
notifications: z.boolean(),
theme: z.enum(['light', 'dark']),
language: z.string().min(2)
});
const [validatedPrefs, setValidatedPrefs] = useQueryState(
'prefs',
parseAsJson(prefsSchema.parse)
);Factory function for creating custom parsers with full type safety.
/**
* Wrap a set of parse/serialize functions into a builder pattern parser
* @param parser - Parser implementation with required parse and serialize functions
*/
function createParser<T>(
parser: Required<Pick<Parser<T>, 'parse' | 'serialize'>> & Partial<Pick<Parser<T>, 'eq'>>
): ParserBuilder<T>;
interface Parser<T> {
/**
* Convert a query string value into a state value.
* If the string value does not represent a valid state value,
* the parser should return null. Throwing an error is also supported.
*/
parse: (value: string) => T | null;
/** Render the state value into a query string value */
serialize?: (value: T) => string;
/**
* Check if two state values are equal.
* This is used when using the clearOnDefault value, to compare the default
* value with the set value. Useful for objects or arrays where referential
* equality check will not work.
*/
eq?: (a: T, b: T) => boolean;
}Usage Examples:
import { createParser } from "nuqs";
// Custom date parser for YYYY-MM-DD format
const parseAsDateString = createParser({
parse: (value: string) => {
const date = new Date(value);
return isNaN(date.getTime()) ? null : date;
},
serialize: (date: Date) => date.toISOString().split('T')[0]
});
// Custom object parser with specific serialization
interface Point {
x: number;
y: number;
}
const parseAsPoint = createParser({
parse: (value: string) => {
const [x, y] = value.split(',').map(Number);
return isNaN(x) || isNaN(y) ? null : { x, y };
},
serialize: (point: Point) => `${point.x},${point.y}`,
eq: (a: Point, b: Point) => a.x === b.x && a.y === b.y
});
const [point, setPoint] = useQueryState('point', parseAsPoint.withDefault({ x: 0, y: 0 }));All parsers implement the builder pattern for configuration and default values.
interface ParserBuilder<T> extends Required<Parser<T>>, Options {
/**
* Specifying a default value makes the hook state non-nullable
* @param defaultValue - The default value to use when query is missing
*/
withDefault(
defaultValue: NonNullable<T>
): Omit<ParserBuilder<T>, 'parseServerSide'> & {
readonly defaultValue: NonNullable<T>;
parseServerSide(value: string | string[] | undefined): NonNullable<T>;
};
/**
* Set history type, shallow routing and scroll restoration options
* @param options - Configuration options for the parser
*/
withOptions<This, Shallow>(this: This, options: Options<Shallow>): This;
/**
* Use the parser in Server Components
* Note: when multiple queries are presented (eg: /?a=1&a=2), only the first will be parsed
* @param value - Query parameter value from page props
*/
parseServerSide(value: string | string[] | undefined): T | null;
}
interface Options<Shallow = unknown> {
/** How the query update affects page history (defaults to 'replace') */
history?: 'replace' | 'push';
/** Scroll to top after a query state update (defaults to false) */
scroll?: boolean;
/** Shallow mode keeps updates client-side only (defaults to true) */
shallow?: Extract<Shallow | boolean, boolean>;
/** Maximum time (ms) between URL query string updates (defaults to 50ms) */
throttleMs?: number;
/** React transition function for observing Server Component loading states */
startTransition?: TransitionStartFunction;
/** Clear key-value pair from URL when setting state to default value */
clearOnDefault?: boolean;
}
type TransitionStartFunction = (callback: () => void) => void;Configuration Examples:
// Parser with default value
const pageParser = parseAsInteger.withDefault(1);
// Parser with options
const searchParser = parseAsString.withOptions({
history: 'push',
throttleMs: 300
});
// Chaining configuration
const statusParser = parseAsStringEnum(['active', 'inactive'] as const)
.withDefault('active')
.withOptions({
clearOnDefault: true,
history: 'replace'
});Utility type for extracting parser return types.
/**
* Type helper to extract the underlying returned data type of a parser
* or of an object describing multiple parsers and their associated keys
*/
type inferParserType<Input> =
Input extends ParserBuilder<any>
? inferSingleParserType<Input>
: Input extends Record<string, ParserBuilder<any>>
? inferParserRecordType<Input>
: never;Usage Examples:
import { type inferParserType, parseAsInteger, parseAsBoolean } from "nuqs";
const intParser = parseAsInteger.withDefault(0);
const boolParser = parseAsBoolean;
type IntType = inferParserType<typeof intParser>; // number
type BoolType = inferParserType<typeof boolParser>; // boolean | null
const parsers = {
count: parseAsInteger.withDefault(0),
enabled: parseAsBoolean,
name: parseAsString
};
type ParsedValues = inferParserType<typeof parsers>;
// { count: number, enabled: boolean | null, name: string | null }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