CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-nuqs

Type-safe search params state manager for Next.js - Like React.useState, but stored in the URL query string

Overall
score

96%

Overview
Eval results
Files

serialization.mddocs/

URL Serialization

Utilities for generating query strings from typed values using parser configuration, enabling programmatic URL generation with the same type safety as query state hooks.

Capabilities

createSerializer

Creates a serializer function that generates query strings from typed values using the same parsers used by query state hooks.

/**
 * Creates a serializer function for generating query strings
 * @param parsers - Object mapping keys to their respective parsers with optional defaults
 * @returns Serializer function with multiple overloads
 */
function createSerializer<Parsers extends Record<string, ParserWithOptionalDefault<any>>>(
  parsers: Parsers
): SerializerFunction<Parsers>;

type ParserWithOptionalDefault<T> = ParserBuilder<T> & { defaultValue?: T };

interface SerializerFunction<Parsers extends Record<string, ParserWithOptionalDefault<any>>> {
  /** Generate a query string for the given values */
  (values: Values<Parsers>): string;
  
  /** Append/amend the query string of the given base with the given values */
  (base: Base, values: Values<Parsers> | null): string;
}

type Base = string | URLSearchParams | URL;
type Values<Parsers extends Record<string, ParserWithOptionalDefault<any>>> = Partial<{
  [K in keyof Parsers]?: ExtractParserType<Parsers[K]>;
}>;

type ExtractParserType<Parser> = Parser extends ParserBuilder<any>
  ? ReturnType<Parser['parseServerSide']>
  : never;

Usage Examples:

import { createSerializer, parseAsString, parseAsInteger, parseAsBoolean } from "nuqs";

// Define the serializer with the same parsers used in components
const searchSerializer = createSerializer({
  q: parseAsString,
  page: parseAsInteger.withDefault(1),
  category: parseAsString,
  featured: parseAsBoolean.withDefault(false)
});

// Generate query strings
const queryString1 = searchSerializer({
  q: 'react',
  page: 2,
  category: 'libraries'
});
// Result: "?q=react&page=2&category=libraries"

const queryString2 = searchSerializer({
  q: 'nextjs',
  featured: true
});
// Result: "?q=nextjs&featured=true"
// Note: page is omitted because it equals the default value

// Clear all parameters
const emptyQuery = searchSerializer({});
// Result: ""

Base URL Modification

Append or modify existing URLs with new query parameters.

/**
 * Append/amend the query string of the given base with the given values
 * Existing search param values will be kept, unless:
 * - the value is null, in which case the search param will be deleted
 * - another value is given for an existing key, in which case the search param will be updated
 */
(base: Base, values: Partial<ExtractParserTypes<Parsers>> | null): string;

Usage Examples:

// Starting with a base URL
const baseUrl = "/search?existing=value&other=param";

// Add/modify specific parameters
const newUrl1 = searchSerializer(baseUrl, {
  q: 'typescript',
  page: 3
});
// Result: "/search?existing=value&other=param&q=typescript&page=3"

// Clear specific parameters with null
const newUrl2 = searchSerializer(baseUrl, {
  q: null,
  category: 'tools'
});
// Result: "/search?existing=value&other=param&category=tools"

// Clear all managed parameters
const clearedUrl = searchSerializer(baseUrl, null);
// Result: "/search?existing=value&other=param"
// Only removes keys defined in the parser schema

// Working with URLSearchParams
const params = new URLSearchParams("?existing=value");
const urlWithParams = searchSerializer(params, {
  q: 'search-term',
  page: 1
});
// Result: "?existing=value&q=search-term&page=1"

// Working with URL objects
const url = new URL("https://example.com/search?existing=value");
const fullUrl = searchSerializer(url, {
  q: 'react'
});
// Result: "https://example.com/search?existing=value&q=react"

Advanced Serialization

Works with all parser types including arrays, enums, and custom parsers.

import { 
  createSerializer, 
  parseAsArrayOf, 
  parseAsStringEnum, 
  parseAsJson,
  parseAsIsoDateTime
} from "nuqs";

// Complex serializer with various data types
const advancedSerializer = createSerializer({
  tags: parseAsArrayOf(parseAsString),
  status: parseAsStringEnum(['active', 'inactive'] as const).withDefault('active'),
  config: parseAsJson<{ theme: string; notifications: boolean }>(),
  createdAfter: parseAsIsoDateTime
});

// Serialize complex data
const complexQuery = advancedSerializer({
  tags: ['react', 'nextjs', 'typescript'],
  status: 'active',
  config: { theme: 'dark', notifications: true },
  createdAfter: new Date('2023-01-01')
});
// Result: "?tags=react,nextjs,typescript&config=%7B%22theme%22:%22dark%22,%22notifications%22:true%7D&createdAfter=2023-01-01T00:00:00.000Z"
// Note: status omitted because it equals default value, config is JSON-encoded and URI-encoded

Link Generation

Common pattern for generating links with query parameters in Next.js applications.

import Link from "next/link";

// Search results component
function SearchResults({ currentQuery, currentPage, totalPages }) {
  // Generate pagination links
  const nextPageUrl = searchSerializer('/search', {
    q: currentQuery,
    page: currentPage + 1
  });
  
  const prevPageUrl = searchSerializer('/search', {
    q: currentQuery,
    page: Math.max(1, currentPage - 1)
  });
  
  // Generate filter links
  const categoryLinks = ['tools', 'libraries', 'frameworks'].map(category => ({
    category,
    url: searchSerializer('/search', {
      q: currentQuery,
      category,
      page: 1 // Reset to first page when changing category
    })
  }));

  return (
    <div>
      {/* Pagination */}
      {currentPage > 1 && (
        <Link href={prevPageUrl}>Previous</Link>
      )}
      {currentPage < totalPages && (
        <Link href={nextPageUrl}>Next</Link>
      )}
      
      {/* Category filters */}
      {categoryLinks.map(({ category, url }) => (
        <Link key={category} href={url}>
          {category}
        </Link>
      ))}
    </div>
  );
}

Default Value Handling

The serializer respects default values and clearOnDefault settings from parsers.

const parserWithDefaults = createSerializer({
  page: parseAsInteger.withDefault(1),
  sort: parseAsString.withDefault('name'),
  enabled: parseAsBoolean.withDefault(false).withOptions({ clearOnDefault: true })
});

// Default values are omitted from URL
const url1 = parserWithDefaults({
  page: 1,        // Omitted (equals default)
  sort: 'name',   // Omitted (equals default)  
  enabled: false  // Omitted (equals default + clearOnDefault: true)
});
// Result: ""

// Non-default values are included
const url2 = parserWithDefaults({
  page: 2,
  sort: 'date',
  enabled: true
});
// Result: "?page=2&sort=date&enabled=true"

Router Integration

Use with Next.js router for programmatic navigation.

import { useRouter } from "next/navigation";

function SearchForm() {
  const router = useRouter();
  
  const handleSearch = (formData: FormData) => {
    const searchParams = {
      q: formData.get('query') as string,
      category: formData.get('category') as string,
      page: 1 // Reset to first page
    };
    
    const url = searchSerializer('/search', searchParams);
    router.push(url);
  };

  return (
    <form action={handleSearch}>
      <input name="query" placeholder="Search..." />
      <select name="category">
        <option value="">All Categories</option>
        <option value="tools">Tools</option>
        <option value="libraries">Libraries</option>
      </select>
      <button type="submit">Search</button>
    </form>
  );
}

Install with Tessl CLI

npx tessl i tessl/npm-nuqs

docs

index.md

parsers.md

query-states.md

serialization.md

server-cache.md

tile.json