nuqs

@contract-kit/nuqs connects contract query schemas to URL-backed state. Use it for search, filters, tabs, sorting, and pagination when the URL should reflect the current view.

bun add @contract-kit/nuqs nuqs

Setup

import { createNQ } from "@contract-kit/nuqs";

export const nq = createNQ();

In Next.js App Router, mount the NuqsAdapter once:

import { NuqsAdapter } from "@contract-kit/nuqs/next/app";

export function Providers({ children }: { children: React.ReactNode }) {
  return <NuqsAdapter>{children}</NuqsAdapter>;
}

URL-backed filters

import { useQuery } from "@tanstack/react-query";
import { parseAsString, parseAsStringLiteral } from "nuqs";
import { listContacts } from "@/app/contracts/contacts";
import { nq } from "@/app/lib/nq";
import { rq } from "@/app/lib/rq";

const contactsSearch = nq(listContacts).query({
  parsers: {
    search: parseAsString,
    group: parseAsStringLiteral(["personal", "work", "family", "other"]),
  },
  history: "replace",
});

function ContactsPage() {
  const [filters, setFilters] = contactsSearch.useState();

  const query = useQuery(
    contactsSearch.toQueryOptions(rq(listContacts), filters, {
      query: { limit: 50, offset: 0 },
    }),
  );

  return null;
}

toQueryOptions(...) composes with @contract-kit/react-query, so URL state and query input stay aligned with the same contract.