[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-70721":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":18,"stars30d":19,"stars90d":16,"forks30d":16,"starsTrendScore":20,"compositeScore":21,"rankGlobal":10,"rankLanguage":10,"license":22,"archived":23,"fork":23,"defaultBranch":24,"hasWiki":23,"hasPages":23,"topics":25,"createdAt":10,"pushedAt":10,"updatedAt":35,"readmeContent":36,"aiSummary":37,"trendingCount":16,"starSnapshotCount":16,"syncStatus":38,"lastSyncTime":39,"discoverSource":40},70721,"nuqs","47ng\u002Fnuqs","47ng","Type-safe search params state manager for React frameworks - Like useState, but stored in the URL query string.","https:\u002F\u002Fnuqs.dev",null,"TypeScript",10536,264,10,31,0,13,28,118,39,111.27,"MIT License",false,"next",[26,27,28,29,30,31,32,33,34],"query-params","react","search-params","state-management","type-safe","type-safety","url-parameters","url-params","url-state","2026-06-12 04:00:56","# nuqs\n\n[![NPM](https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fv\u002Fnuqs?color=red)](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fnuqs)\n[![MIT License](https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Flicense\u002F47ng\u002Fnuqs.svg?color=blue)](https:\u002F\u002Fgithub.com\u002F47ng\u002Fnuqs\u002Fblob\u002Fnext\u002FLICENSE)\n[![GitHub Sponsors](https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Fsponsors\u002Ffranky47?color=%23db61a2&label=Sponsors)](https:\u002F\u002Fgithub.com\u002Fsponsors\u002Ffranky47)\n[![CI\u002FCD](https:\u002F\u002Fgithub.com\u002F47ng\u002Fnuqs\u002Factions\u002Fworkflows\u002Fci-cd.yml\u002Fbadge.svg?branch=next)](https:\u002F\u002Fgithub.com\u002F47ng\u002Fnuqs\u002Factions\u002Fworkflows\u002Fci-cd.yml)\n[![Depfu](https:\u002F\u002Fbadges.depfu.com\u002Fbadges\u002Facad53fa2b09b1e435a19d6d18f29af4\u002Fcount.svg)](https:\u002F\u002Fdepfu.com\u002Fgithub\u002F47ng\u002Fnuqs?project_id=22104)\n\nType-safe search params state manager for React frameworks. Like `useState`, but stored in the URL query string.\n\n## Features\n\n- 🔀 **new:** Supports Next.js (`app` and `pages` routers), plain React (SPA), Remix, React Router, TanStack Router, and custom routers via [adapters](#adapters)\n- 🧘‍♀️ Simple: the URL is the source of truth\n- 🕰 Replace history or [append](#history) to use the Back button to navigate state updates\n- ⚡️ Built-in [parsers](#parsing) for common state types (integer, float, boolean, Date, and more). Create your own parsers for custom types & pretty URLs\n- ♊️ Related querystrings with [`useQueryStates`](#usequerystates)\n- 📡 [Shallow mode](#shallow) by default for URL query updates, opt-in to notify server components\n- 🗃 [Server cache](#accessing-searchparams-in-server-components) for type-safe searchParams access in nested server components\n- ⌛️ Support for [`useTransition`](#transitions) to get loading states on server updates\n\n## Documentation\n\nRead the complete documentation at [nuqs.dev](https:\u002F\u002Fnuqs.dev).\n\n## Installation\n\n```shell\npnpm add nuqs\n```\n\n```shell\nyarn add nuqs\n```\n\n```shell\nnpm install nuqs\n```\n\n## Adapters\n\nYou will need to wrap your React component tree with an adapter for your framework. _(expand the appropriate section below)_\n\n\u003Cdetails>\u003Csummary>▲ Next.js (app router)\u003C\u002Fsummary>\n\n> Supported Next.js versions: `>=14.2.0`. For older versions, install `nuqs@^1` (which doesn't need this adapter code).\n\n```tsx\n\u002F\u002F src\u002Fapp\u002Flayout.tsx\nimport { NuqsAdapter } from 'nuqs\u002Fadapters\u002Fnext\u002Fapp'\nimport { type ReactNode } from 'react'\n\nexport default function RootLayout({ children }: { children: ReactNode }) {\n  return (\n    \u003Chtml>\n      \u003Cbody>\n        \u003CNuqsAdapter>{children}\u003C\u002FNuqsAdapter>\n      \u003C\u002Fbody>\n    \u003C\u002Fhtml>\n  )\n}\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\u003Csummary>▲ Next.js (pages router)\u003C\u002Fsummary>\n\n> Supported Next.js versions: `>=14.2.0`. For older versions, install `nuqs@^1` (which doesn't need this adapter code).\n\n```tsx\n\u002F\u002F src\u002Fpages\u002F_app.tsx\nimport type { AppProps } from 'next\u002Fapp'\nimport { NuqsAdapter } from 'nuqs\u002Fadapters\u002Fnext\u002Fpages'\n\nexport default function MyApp({ Component, pageProps }: AppProps) {\n  return (\n    \u003CNuqsAdapter>\n      \u003CComponent {...pageProps} \u002F>\n    \u003C\u002FNuqsAdapter>\n  )\n}\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\u003Csummary>⚛️ Plain React (SPA)\u003C\u002Fsummary>\n\nExample: via Vite or create-react-app.\n\n```tsx\nimport { NuqsAdapter } from 'nuqs\u002Fadapters\u002Freact'\n\ncreateRoot(document.getElementById('root')!).render(\n  \u003CNuqsAdapter>\n    \u003CApp \u002F>\n  \u003C\u002FNuqsAdapter>\n)\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\u003Csummary>💿 Remix\u003C\u002Fsummary>\n\n> Supported Remix versions: `@remix-run\u002Freact@>=2`\n\n```tsx\n\u002F\u002F app\u002Froot.tsx\nimport { NuqsAdapter } from 'nuqs\u002Fadapters\u002Fremix'\n\n\u002F\u002F ...\n\nexport default function App() {\n  return (\n    \u003CNuqsAdapter>\n      \u003COutlet \u002F>\n    \u003C\u002FNuqsAdapter>\n  )\n}\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\u003Csummary>\u003Cspan style=\"width:16px;height:16px;background:#fff;border-radius:2px;\">\u003Cimg width=\"16px\" height=\"16px\" src=\"https:\u002F\u002Freactrouter.com\u002F_brand\u002FReact%20Router%20Brand%20Assets\u002FReact%20Router%20Logo\u002FLight.svg\" \u002F>\u003C\u002Fspan> React Router v6\n\u003C\u002Fsummary>\n\n> Supported React Router versions: `react-router-dom@^6`\n\n```tsx\nimport { NuqsAdapter } from 'nuqs\u002Fadapters\u002Freact-router\u002Fv6'\nimport { createBrowserRouter, RouterProvider } from 'react-router-dom'\nimport App from '.\u002FApp'\n\nconst router = createBrowserRouter([\n  {\n    path: '\u002F',\n    element: \u003CApp \u002F>\n  }\n])\n\nexport function ReactRouter() {\n  return (\n    \u003CNuqsAdapter>\n      \u003CRouterProvider router={router} \u002F>\n    \u003C\u002FNuqsAdapter>\n  )\n}\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\u003Csummary>\u003Cspan style=\"width:16px;height:16px;background:#fff;border-radius:2px;\">\u003Cimg width=\"16px\" height=\"16px\" src=\"https:\u002F\u002Freactrouter.com\u002F_brand\u002FReact%20Router%20Brand%20Assets\u002FReact%20Router%20Logo\u002FLight.svg\" \u002F>\u003C\u002Fspan> React Router v7\n\u003C\u002Fsummary>\n\n> Supported React Router versions: `react-router@^7`\n\n```tsx\n\u002F\u002F app\u002Froot.tsx\nimport { NuqsAdapter } from 'nuqs\u002Fadapters\u002Freact-router\u002Fv7'\nimport { Outlet } from 'react-router'\n\n\u002F\u002F ...\n\nexport default function App() {\n  return (\n    \u003CNuqsAdapter>\n      \u003COutlet \u002F>\n    \u003C\u002FNuqsAdapter>\n  )\n}\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\u003Csummary>🏝️ TanStack Router\u003C\u002Fsummary>\n\n> Supported TanStack Router versions: `@tanstack\u002Freact-router@^1`\n> Note: TanStack Router support is experimental and does not yet cover TanStack Start.\n\n```tsx\n\u002F\u002F src\u002Froutes\u002F__root.tsx\nimport { NuqsAdapter } from 'nuqs\u002Fadapters\u002Ftanstack-router'\nimport { Outlet, createRootRoute } from '@tanstack\u002Freact-router'\n\nexport const Route = createRootRoute({\n  component: () => (\n    \u003C>\n      \u003CNuqsAdapter>\n        \u003COutlet \u002F>\n      \u003C\u002FNuqsAdapter>\n    \u003C\u002F>\n  )\n})\n```\n\n\u003C\u002Fdetails>\n\n## Usage\n\n```tsx\n'use client' \u002F\u002F Only works in client components\n\nimport { useQueryState } from 'nuqs'\n\nexport default () => {\n  const [name, setName] = useQueryState('name')\n  return (\n    \u003C>\n      \u003Ch1>Hello, {name || 'anonymous visitor'}!\u003C\u002Fh1>\n      \u003Cinput value={name || ''} onChange={e => setName(e.target.value)} \u002F>\n      \u003Cbutton onClick={() => setName(null)}>Clear\u003C\u002Fbutton>\n    \u003C\u002F>\n  )\n}\n```\n\n![](https:\u002F\u002Fraw.githubusercontent.com\u002F47ng\u002Fnuqs\u002Fnext\u002FuseQueryState.gif)\n\n`useQueryState` takes one required argument: the key to use in the query string.\n\nLike `React.useState`, it returns an array with the value present in the query\nstring as a string (or `null` if none was found), and a state updater function.\n\nExample outputs for our hello world example:\n\n| URL          | name value | Notes                                                             |\n| ------------ | ---------- | ----------------------------------------------------------------- |\n| `\u002F`          | `null`     | No `name` key in URL                                              |\n| `\u002F?name=`    | `''`       | Empty string                                                      |\n| `\u002F?name=foo` | `'foo'`    |\n| `\u002F?name=2`   | `'2'`      | Always returns a string by default, see [Parsing](#parsing) below |\n\n## Parsing\n\nIf your state type is not a string, you must pass a parsing function in the\nsecond argument object.\n\nWe provide parsers for common and more advanced object types:\n\n```ts\nimport {\n  parseAsString,\n  parseAsInteger,\n  parseAsFloat,\n  parseAsBoolean,\n  parseAsTimestamp,\n  parseAsIsoDateTime,\n  parseAsArrayOf,\n  parseAsJson,\n  parseAsStringEnum,\n  parseAsStringLiteral,\n  parseAsNumberLiteral\n} from 'nuqs'\n\nuseQueryState('tag') \u002F\u002F defaults to string\nuseQueryState('count', parseAsInteger)\nuseQueryState('brightness', parseAsFloat)\nuseQueryState('darkMode', parseAsBoolean)\nuseQueryState('after', parseAsTimestamp) \u002F\u002F state is a Date\nuseQueryState('date', parseAsIsoDateTime) \u002F\u002F state is a Date\nuseQueryState('array', parseAsArrayOf(parseAsInteger)) \u002F\u002F state is number[]\nuseQueryState('json', parseAsJson\u003CPoint>()) \u002F\u002F state is a Point\n\n\u002F\u002F Enums (string-based only)\nenum Direction {\n  up = 'UP',\n  down = 'DOWN',\n  left = 'LEFT',\n  right = 'RIGHT'\n}\n\nconst [direction, setDirection] = useQueryState(\n  'direction',\n  parseAsStringEnum\u003CDirection>(Object.values(Direction)) \u002F\u002F pass a list of allowed values\n    .withDefault(Direction.up)\n)\n\n\u002F\u002F Literals (string-based only)\nconst colors = ['red', 'green', 'blue'] as const\n\nconst [color, setColor] = useQueryState(\n  'color',\n  parseAsStringLiteral(colors) \u002F\u002F pass a readonly list of allowed values\n    .withDefault('red')\n)\n\n\u002F\u002F Literals (number-based only)\nconst diceSides = [1, 2, 3, 4, 5, 6] as const\n\nconst [side, setSide] = useQueryState(\n  'side',\n  parseAsNumberLiteral(diceSides) \u002F\u002F pass a readonly list of allowed values\n    .withDefault(4)\n)\n```\n\nYou may pass a custom set of `parse` and `serialize` functions:\n\n```tsx\nimport { useQueryState } from 'nuqs'\n\nexport default () => {\n  const [hex, setHex] = useQueryState('hex', {\n    \u002F\u002F TypeScript will automatically infer it's a number\n    \u002F\u002F based on what `parse` returns.\n    parse: (query: string) => parseInt(query, 16),\n    serialize: value => value.toString(16)\n  })\n}\n```\n\n## Default value\n\nWhen the query string is not present in the URL, the default behaviour is to\nreturn `null` as state.\n\nIt can make state updating and UI rendering tedious. Take this example of a simple counter stored in the URL:\n\n```tsx\nimport { useQueryState, parseAsInteger } from 'nuqs'\n\nexport default () => {\n  const [count, setCount] = useQueryState('count', parseAsInteger)\n  return (\n    \u003C>\n      \u003Cpre>count: {count}\u003C\u002Fpre>\n      \u003Cbutton onClick={() => setCount(0)}>Reset\u003C\u002Fbutton>\n      {\u002F* handling null values in setCount is annoying: *\u002F}\n      \u003Cbutton onClick={() => setCount(c => c ?? 0 + 1)}>+\u003C\u002Fbutton>\n      \u003Cbutton onClick={() => setCount(c => c ?? 0 - 1)}>-\u003C\u002Fbutton>\n      \u003Cbutton onClick={() => setCount(null)}>Clear\u003C\u002Fbutton>\n    \u003C\u002F>\n  )\n}\n```\n\nYou can specify a default value to be returned in this case:\n\n```ts\nconst [count, setCount] = useQueryState('count', parseAsInteger.withDefault(0))\n\nconst increment = () => setCount(c => c + 1) \u002F\u002F c will never be null\nconst decrement = () => setCount(c => c - 1) \u002F\u002F c will never be null\nconst clearCount = () => setCount(null) \u002F\u002F Remove query from the URL\n```\n\nNote: the default value is internal to React, it will **not** be written to the\nURL.\n\nSetting the state to `null` will remove the key in the query string and set the\nstate to the default value.\n\n## Options\n\n### History\n\nBy default, state updates are done by replacing the current history entry with\nthe updated query when state changes.\n\nYou can see this as a sort of `git squash`, where all state-changing\noperations are merged into a single history value.\n\nYou can also opt-in to push a new history item for each state change,\nper key, which will let you use the Back button to navigate state\nupdates:\n\n```ts\n\u002F\u002F Default: replace current history with new state\nuseQueryState('foo', { history: 'replace' })\n\n\u002F\u002F Append state changes to history:\nuseQueryState('foo', { history: 'push' })\n```\n\nAny other value for the `history` option will fallback to the default.\n\nYou can also override the history mode when calling the state updater function:\n\n```ts\nconst [query, setQuery] = useQueryState('q', { history: 'push' })\n\n\u002F\u002F This overrides the hook declaration setting:\nsetQuery(null, { history: 'replace' })\n```\n\n### Shallow\n\n> Note: this feature only applies to Next.js\n\nBy default, query state updates are done in a _client-first_ manner: there are\nno network calls to the server.\n\nThis is equivalent to the `shallow` option of the Next.js pages router set to `true`,\nor going through the experimental [`windowHistorySupport`](https:\u002F\u002Fgithub.com\u002Fvercel\u002Fnext.js\u002Fdiscussions\u002F48110)\nflag in the app router.\n\nTo opt-in to query updates notifying the server (to re-run `getServerSideProps`\nin the pages router and re-render Server Components on the app router),\nyou can set `shallow` to `false`:\n\n```ts\nconst [state, setState] = useQueryState('foo', { shallow: false })\n\n\u002F\u002F You can also pass the option on calls to setState:\nsetState('bar', { shallow: false })\n```\n\n### Throttling URL updates\n\nBecause of browsers rate-limiting the History API, internal updates to the\nURL are queued and throttled to a default of 50ms, which seems to satisfy\nmost browsers even when sending high-frequency query updates, like binding\nto a text input or a slider.\n\nSafari's rate limits are much stricter and would require a throttle of around 340ms.\nIf you end up needing a longer time between updates, you can specify it in the\noptions:\n\n```ts\nuseQueryState('foo', {\n  \u002F\u002F Send updates to the server maximum once every second\n  shallow: false,\n  throttleMs: 1000\n})\n\n\u002F\u002F You can also pass the option on calls to setState:\nsetState('bar', { throttleMs: 1000 })\n```\n\n> Note: the state returned by the hook is always updated instantly, to keep UI responsive.\n> Only changes to the URL, and server requests when using `shallow: false`, are throttled.\n\nIf multiple hooks set different throttle values on the same event loop tick,\nthe highest value will be used. Also, values lower than 50ms will be ignored,\nto avoid rate-limiting issues. [Read more](https:\u002F\u002Ffrancoisbest.com\u002Fposts\u002F2023\u002Fstoring-react-state-in-the-url-with-nextjs#batching--throttling).\n\n### Transitions\n\nWhen combined with `shallow: false`, you can use the `useTransition` hook to get\nloading states while the server is re-rendering server components with the\nupdated URL.\n\nPass in the `startTransition` function from `useTransition` to the options\nto enable this behaviour:\n\n```tsx\n'use client'\n\nimport React from 'react'\nimport { useQueryState, parseAsString } from 'nuqs'\n\nfunction ClientComponent({ data }) {\n  \u002F\u002F 1. Provide your own useTransition hook:\n  const [isLoading, startTransition] = React.useTransition()\n  const [query, setQuery] = useQueryState(\n    'query',\n    \u002F\u002F 2. Pass the `startTransition` as an option:\n    parseAsString.withOptions({\n      startTransition,\n      shallow: false \u002F\u002F opt-in to notify the server (Next.js only)\n    })\n  )\n  \u002F\u002F 3. `isLoading` will be true while the server is re-rendering\n  \u002F\u002F and streaming RSC payloads, when the query is updated via `setQuery`.\n\n  \u002F\u002F Indicate loading state\n  if (isLoading) return \u003Cdiv>Loading...\u003C\u002Fdiv>\n\n  \u002F\u002F Normal rendering with data\n  return \u003Cdiv>{\u002F*...*\u002F}\u003C\u002Fdiv>\n}\n```\n\n## Configuring parsers, default value & options\n\nYou can use a builder pattern to facilitate specifying all of those things:\n\n```ts\nuseQueryState(\n  'counter',\n  parseAsInteger.withDefault(0).withOptions({\n    history: 'push',\n    shallow: false\n  })\n)\n```\n\nYou can get this pattern for your custom parsers too, and compose them\nwith others:\n\n```ts\nimport { createParser, parseAsHex } from 'nuqs'\n\n\u002F\u002F Wrapping your parser\u002Fserializer in `createParser`\n\u002F\u002F gives it access to the builder pattern & server-side\n\u002F\u002F parsing capabilities:\nconst hexColorSchema = createParser({\n  parse(query) {\n    if (query.length !== 6) {\n      return null \u002F\u002F always return null for invalid inputs\n    }\n    return {\n      \u002F\u002F When composing other parsers, they may return null too.\n      r: parseAsHex.parse(query.slice(0, 2)) ?? 0x00,\n      g: parseAsHex.parse(query.slice(2, 4)) ?? 0x00,\n      b: parseAsHex.parse(query.slice(4)) ?? 0x00\n    }\n  },\n  serialize({ r, g, b }) {\n    return (\n      parseAsHex.serialize(r) +\n      parseAsHex.serialize(g) +\n      parseAsHex.serialize(b)\n    )\n  }\n})\n  \u002F\u002F Eg: set common options directly\n  .withOptions({ history: 'push' })\n\n\u002F\u002F Or on usage:\nuseQueryState(\n  'tribute',\n  hexColorSchema.withDefault({\n    r: 0x66,\n    g: 0x33,\n    b: 0x99\n  })\n)\n```\n\nNote: see this example running in the [hex-colors demo](\u003C.\u002Fpackages\u002Fdocs\u002Fsrc\u002Fapp\u002Fplayground\u002F(demos)\u002Fhex-colors\u002Fpage.tsx>).\n\n## Multiple Queries (batching)\n\nYou can call as many state update function as needed in a single event loop\ntick, and they will be applied to the URL asynchronously:\n\n```ts\nconst MultipleQueriesDemo = () => {\n  const [lat, setLat] = useQueryState('lat', parseAsFloat)\n  const [lng, setLng] = useQueryState('lng', parseAsFloat)\n  const randomCoordinates = React.useCallback(() => {\n    setLat(Math.random() * 180 - 90)\n    setLng(Math.random() * 360 - 180)\n  }, [])\n}\n```\n\nIf you wish to know when the URL has been updated, and what it contains, you can\nawait the Promise returned by the state updater function, which gives you the\nupdated URLSearchParameters object:\n\n```ts\nconst randomCoordinates = React.useCallback(() => {\n  setLat(42)\n  return setLng(12)\n}, [])\n\nrandomCoordinates().then((search: URLSearchParams) => {\n  search.get('lat') \u002F\u002F 42\n  search.get('lng') \u002F\u002F 12, has been queued and batch-updated\n})\n```\n\n\u003Cdetails>\n\u003Csummary>\u003Cem>Implementation details (Promise caching)\u003C\u002Fem>\u003C\u002Fsummary>\n\nThe returned Promise is cached until the next flush to the URL occurs,\nso all calls to a setState (of any hook) in the same event loop tick will\nreturn the same Promise reference.\n\nDue to throttling of calls to the Web History API, the Promise may be cached\nfor several ticks. Batched updates will be merged and flushed once to the URL.\nThis means not every setState will reflect to the URL, if another one comes\noverriding it before flush occurs.\n\nThe returned React state will reflect all set values instantly,\nto keep UI responsive.\n\n---\n\n\u003C\u002Fdetails>\n\n## `useQueryStates`\n\nFor query keys that should always move together, you can use `useQueryStates`\nwith an object containing each key's type:\n\n```ts\nimport { useQueryStates, parseAsFloat } from 'nuqs'\n\nconst [coordinates, setCoordinates] = useQueryStates(\n  {\n    lat: parseAsFloat.withDefault(45.18),\n    lng: parseAsFloat.withDefault(5.72)\n  },\n  {\n    history: 'push'\n  }\n)\n\nconst { lat, lng } = coordinates\n\n\u002F\u002F Set all (or a subset of) the keys in one go:\nconst search = await setCoordinates({\n  lat: Math.random() * 180 - 90,\n  lng: Math.random() * 360 - 180\n})\n```\n\n## Loaders\n\nTo parse search params as a one-off operation, you can use a **loader function**:\n\n```tsx\nimport { createLoader } from 'nuqs' \u002F\u002F or 'nuqs\u002Fserver'\n\nconst searchParams = {\n  q: parseAsString,\n  page: parseAsInteger.withDefault(1)\n}\n\nconst loadSearchParams = createLoader(searchParams)\n\nconst { q, page } = loadSearchParams('?q=hello&page=2')\n```\n\nIt accepts various types of inputs (strings, URL, URLSearchParams, Request, Promises, etc.). [Read more](https:\u002F\u002Fnuqs.dev\u002Fdocs\u002Fserver-side#loaders)\n\nSee the [server-side parsing demo](\u003C.\u002Fpackages\u002Fdocs\u002Fsrc\u002Fapp\u002Fplayground\u002F(demos)\u002Fpagination>)\nfor a live example showing how to reuse parser configurations between\nclient and server code.\n\n## Accessing searchParams in Server Components\n\nIf you wish to access the searchParams in a deeply nested Server Component\n(ie: not in the Page component), you can use `createSearchParamsCache`\nto do so in a type-safe manner.\n\n> Note: parsers **don't validate** your data. If you expect positive integers\n> or JSON-encoded objects of a particular shape, you'll need to feed the result\n> of the parser to a schema validation library, like [Zod](https:\u002F\u002Fzod.dev).\n\n```tsx\n\u002F\u002F searchParams.ts\nimport {\n  createSearchParamsCache,\n  parseAsInteger,\n  parseAsString\n} from 'nuqs\u002Fserver'\n\u002F\u002F Note: import from 'nuqs\u002Fserver' to avoid the \"use client\" directive\n\nexport const searchParamsCache = createSearchParamsCache({\n  \u002F\u002F List your search param keys and associated parsers here:\n  q: parseAsString.withDefault(''),\n  maxResults: parseAsInteger.withDefault(10)\n})\n\n\u002F\u002F page.tsx\nimport { searchParamsCache } from '.\u002FsearchParams'\n\nexport default function Page({\n  searchParams\n}: {\n  searchParams: Record\u003Cstring, string | string[] | undefined>\n}) {\n  \u002F\u002F ⚠️ Don't forget to call `parse` here.\n  \u002F\u002F You can access type-safe values from the returned object:\n  const { q: query } = searchParamsCache.parse(searchParams)\n  return (\n    \u003Cdiv>\n      \u003Ch1>Search Results for {query}\u003C\u002Fh1>\n      \u003CResults \u002F>\n    \u003C\u002Fdiv>\n  )\n}\n\nfunction Results() {\n  \u002F\u002F Access type-safe search params in children server components:\n  const maxResults = searchParamsCache.get('maxResults')\n  return \u003Cspan>Showing up to {maxResults} results\u003C\u002Fspan>\n}\n```\n\nThe cache will only be valid for the current page render\n(see React's [`cache`](https:\u002F\u002Freact.dev\u002Freference\u002Freact\u002Fcache) function).\n\nNote: the cache only works for **server components**, but you may share your\nparser declaration with `useQueryStates` for type-safety in client components:\n\n```tsx\n\u002F\u002F searchParams.ts\nimport { parseAsFloat, createSearchParamsCache } from 'nuqs\u002Fserver'\n\nexport const coordinatesParsers = {\n  lat: parseAsFloat.withDefault(45.18),\n  lng: parseAsFloat.withDefault(5.72)\n}\nexport const coordinatesCache = createSearchParamsCache(coordinatesParsers)\n\n\u002F\u002F page.tsx\nimport { coordinatesCache } from '.\u002FsearchParams'\nimport { Server } from '.\u002Fserver'\nimport { Client } from '.\u002Fclient'\n\nexport default async function Page({ searchParams }) {\n  await coordinatesCache.parse(searchParams)\n  return (\n    \u003C>\n      \u003CServer \u002F>\n      \u003CSuspense>\n        \u003CClient \u002F>\n      \u003C\u002FSuspense>\n    \u003C\u002F>\n  )\n}\n\n\u002F\u002F server.tsx\nimport { coordinatesCache } from '.\u002FsearchParams'\n\nexport function Server() {\n  const { lat, lng } = coordinatesCache.all()\n  \u002F\u002F or access keys individually:\n  const lat = coordinatesCache.get('lat')\n  const lng = coordinatesCache.get('lng')\n  return (\n    \u003Cspan>\n      Latitude: {lat} - Longitude: {lng}\n    \u003C\u002Fspan>\n  )\n}\n\n\u002F\u002F client.tsx\n\u002F\u002F prettier-ignore\n;'use client'\n\nimport { useQueryStates } from 'nuqs'\nimport { coordinatesParsers } from '.\u002FsearchParams'\n\nexport function Client() {\n  const [{ lat, lng }, setCoordinates] = useQueryStates(coordinatesParsers)\n  \u002F\u002F ...\n}\n```\n\n## Serializer helper\n\nTo populate `\u003CLink>` components with state values, you can use the `createSerializer`\nhelper.\n\nPass it an object describing your search params, and it will give you a function\nto call with values, that generates a query string serialized as the hooks would do.\n\nExample:\n\n```ts\nimport {\n  createSerializer,\n  parseAsInteger,\n  parseAsIsoDateTime,\n  parseAsString,\n  parseAsStringLiteral\n} from 'nuqs\u002Fserver'\n\nconst searchParams = {\n  search: parseAsString,\n  limit: parseAsInteger,\n  from: parseAsIsoDateTime,\n  to: parseAsIsoDateTime,\n  sortBy: parseAsStringLiteral(['asc', 'desc'])\n}\n\n\u002F\u002F Create a serializer function by passing the description of the search params to accept\nconst serialize = createSerializer(searchParams)\n\n\u002F\u002F Then later, pass it some values (a subset) and render them to a query string\nserialize({\n  search: 'foo bar',\n  limit: 10,\n  from: new Date('2024-01-01'),\n  \u002F\u002F here, we omit `to`, which won't be added\n  sortBy: null \u002F\u002F null values are also not rendered\n})\n\u002F\u002F ?search=foo+bar&limit=10&from=2024-01-01T00:00:00.000Z\n```\n\n### Base parameter\n\nThe returned `serialize` function can take a base parameter over which to\nappend\u002Famend the search params:\n\n```ts\nserialize('\u002Fpath?baz=qux', { foo: 'bar' }) \u002F\u002F \u002Fpath?baz=qux&foo=bar\n\nconst search = new URLSearchParams('?baz=qux')\nserialize(search, { foo: 'bar' }) \u002F\u002F ?baz=qux&foo=bar\n\nconst url = new URL('https:\u002F\u002Fexample.com\u002Fpath?baz=qux')\nserialize(url, { foo: 'bar' }) \u002F\u002F https:\u002F\u002Fexample.com\u002Fpath?baz=qux&foo=bar\n\n\u002F\u002F Passing null removes existing values\nserialize('?remove=me', { foo: 'bar', remove: null }) \u002F\u002F ?foo=bar\n```\n\n## Parser type inference\n\nTo access the underlying type returned by a parser, you can use the\n`inferParserType` type helper:\n\n```ts\nimport { parseAsInteger, type inferParserType } from 'nuqs' \u002F\u002F or 'nuqs\u002Fserver'\n\nconst intNullable = parseAsInteger\nconst intNonNull = parseAsInteger.withDefault(0)\n\ninferParserType\u003Ctypeof intNullable> \u002F\u002F number | null\ninferParserType\u003Ctypeof intNonNull> \u002F\u002F number\n```\n\nFor an object describing parsers (that you'd pass to `createSearchParamsCache`\nor to `useQueryStates`, `inferParserType` will\nreturn the type of the object with the parsers replaced by their inferred types:\n\n```ts\nimport { parseAsBoolean, parseAsInteger, type inferParserType } from 'nuqs' \u002F\u002F or 'nuqs\u002Fserver'\n\nconst parsers = {\n  a: parseAsInteger,\n  b: parseAsBoolean.withDefault(false)\n}\n\ninferParserType\u003Ctypeof parsers>\n\u002F\u002F { a: number | null, b: boolean }\n```\n\n## Testing\n\nSince nuqs v2, you can use a testing adapter to unit-test components using\n`useQueryState` and `useQueryStates` in isolation, without needing to mock\nyour framework or router.\n\nHere's an example using Testing Library and Vitest:\n\n```tsx\nimport { render, screen } from '@testing-library\u002Freact'\nimport userEvent from '@testing-library\u002Fuser-event'\nimport { NuqsTestingAdapter, type UrlUpdateEvent } from 'nuqs\u002Fadapters\u002Ftesting'\nimport { describe, expect, it, vi } from 'vitest'\nimport { CounterButton } from '.\u002Fcounter-button'\n\nit('should increment the count when clicked', async () => {\n  const user = userEvent.setup()\n  const onUrlUpdate = vi.fn\u003C[UrlUpdateEvent]>()\n  render(\u003CCounterButton \u002F>, {\n    \u002F\u002F Setup the test by passing initial search params \u002F querystring,\n    \u002F\u002F and give it a function to call on URL updates\n    wrapper: ({ children }) => (\n      \u003CNuqsTestingAdapter searchParams=\"?count=42\" onUrlUpdate={onUrlUpdate}>\n        {children}\n      \u003C\u002FNuqsTestingAdapter>\n    )\n  })\n  \u002F\u002F Initial state assertions: there's a clickable button displaying the count\n  const button = screen.getByRole('button')\n  expect(button).toHaveTextContent('count is 42')\n  \u002F\u002F Act\n  await user.click(button)\n  \u002F\u002F Assert changes in the state and in the (mocked) URL\n  expect(button).toHaveTextContent('count is 43')\n  expect(onUrlUpdate).toHaveBeenCalledOnce()\n  expect(onUrlUpdate.mock.calls[0][0].queryString).toBe('?count=43')\n  expect(onUrlUpdate.mock.calls[0][0].searchParams.get('count')).toBe('43')\n  expect(onUrlUpdate.mock.calls[0][0].options.history).toBe('push')\n})\n```\n\nSee [#259](https:\u002F\u002Fgithub.com\u002F47ng\u002Fnuqs\u002Fissues\u002F259) for more testing-related discussions.\n\n## Debugging\n\nYou can enable debug logs in the browser by setting the `debug` item in localStorage\nto `nuqs`, and reload the page.\n\n```js\n\u002F\u002F In your devtools:\nlocalStorage.setItem('debug', 'nuqs')\n```\n\n> Note: unlike the `debug` package, this will not work with wildcards, but\n> you can combine it: `localStorage.setItem('debug', '*,nuqs')`\n\nLog lines will be prefixed with `[nuqs]` for `useQueryState` and `[nuq+]` for\n`useQueryStates`, along with other internal debug logs.\n\nUser timings markers are also recorded, for advanced performance analysis using\nyour browser's devtools.\n\nProviding debug logs when opening an [issue](https:\u002F\u002Fgithub.com\u002F47ng\u002Fnuqs\u002Fissues)\nis always appreciated. 🙏\n\n### SEO\n\nIf your page uses query strings for local-only state, you should add a\ncanonical URL to your page, to tell SEO crawlers to ignore the query string\nand index the page without it.\n\nIn the app router, this is done via the metadata object:\n\n```ts\nimport type { Metadata } from 'next'\n\nexport const metadata: Metadata = {\n  alternates: {\n    canonical: '\u002Furl\u002Fpath\u002Fwithout\u002Fquerystring'\n  }\n}\n```\n\nIf however the query string is defining what content the page is displaying\n(eg: YouTube's watch URLs, like `https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=dQw4w9WgXcQ`),\nyour canonical URL should contain relevant query strings, and you can still\nuse your parsers to read it, and to serialize the canonical URL:\n\n```ts\n\u002F\u002F page.tsx\nimport type { Metadata, ResolvingMetadata } from 'next'\nimport { notFound } from 'next\u002Fnavigation'\nimport {\n  createParser,\n  parseAsString,\n  createLoader,\n  createSerializer,\n  type SearchParams,\n  type UrlKeys\n} from 'nuqs\u002Fserver'\n\nconst youTubeVideoIdRegex = \u002F^[^\"&?\\\u002F\\s]{11}$\u002Fi\nconst youTubeSearchParams = {\n  videoId: createParser({\n    parse(query) {\n      if (!youTubeVideoIdRegex.test(query)) {\n        return null\n      }\n      return query\n    },\n    serialize(videoId) {\n      return videoId\n    }\n  })\n}\nconst youTubeUrlKeys: UrlKeys\u003Ctypeof youTubeSearchParams> = {\n  videoId: 'v'\n}\nconst loadYouTubeSearchParams = createLoader(youTubeSearchParams, {\n  urlKeys: youTubeUrlKeys\n})\nconst serializeYouTubeSearchParams = createSerializer(youTubeSearchParams, {\n  urlKeys: youTubeUrlKeys\n})\n\n\u002F\u002F --\n\ntype Props = {\n  searchParams: Promise\u003CSearchParams>\n}\n\nexport async function generateMetadata({\n  searchParams\n}: Props): Promise\u003CMetadata> {\n  const { videoId } = await loadYouTubeSearchParams(searchParams)\n  if (!videoId) {\n    notFound()\n  }\n  return {\n    alternates: {\n      canonical: serializeYouTubeSearchParams('\u002Fwatch', { videoId })\n      \u002F\u002F \u002Fwatch?v=dQw4w9WgXcQ\n    }\n  }\n}\n```\n\n### Lossy serialization\n\nIf your serializer loses precision or doesn't accurately represent\nthe underlying state value, you will lose this precision when\nreloading the page or restoring state from the URL (eg: on navigation).\n\nExample:\n\n```ts\nconst geoCoordParser = {\n  parse: parseFloat,\n  serialize: v => v.toFixed(4) \u002F\u002F Loses precision\n}\n\nconst [lat, setLat] = useQueryState('lat', geoCoordParser)\n```\n\nHere, setting a latitude of 1.23456789 will render a URL query string\nof `lat=1.2345`, while the internal `lat` state will be correctly\nset to 1.23456789.\n\nUpon reloading the page, the state will be incorrectly set to 1.2345.\n\n## License\n\n[MIT](https:\u002F\u002Fgithub.com\u002F47ng\u002Fnuqs\u002Fblob\u002Fnext\u002FLICENSE)\n\nMade with ❤️ by [François Best](https:\u002F\u002Ffrancoisbest.com)\n\nUsing this package at work ? [Sponsor me](https:\u002F\u002Fgithub.com\u002Fsponsors\u002Ffranky47)\nto help with support and maintenance.\n\n\u003Cdiv>\nnuqs is part of the &nbsp;\u003Ca href=\"https:\u002F\u002Fvercel.com\u002Foss\">\u003Cimg alt=\"Vercel OSS Program\" src=\"https:\u002F\u002Fvercel.com\u002Foss\u002Fprogram-badge.svg\" \u002F>\u003C\u002Fa>&nbsp;\u003Csmall>\u003Ca href=\"https:\u002F\u002Fvercel.com\u002Fblog\u002Fspring25-oss-program\">(spring 2025 cohort)\u003C\u002Fa>\u003C\u002Fsmall>\n\u003C\u002Fdiv>\n\n\u003Cbr\u002F>\n\n![Project analytics and stats](https:\u002F\u002Frepobeats.axiom.co\u002Fapi\u002Fembed\u002F3ee740e4729dce3992bfa8c74645cfebad8ba034.svg 'Repobeats analytics image')\n","nuqs 是一个为 React 框架设计的类型安全搜索参数状态管理库，类似于 `useState` 但将状态存储在 URL 查询字符串中。它支持多种框架如 Next.js、Remix 和 React Router 等，并通过适配器提供灵活的集成方式。nuqs 的核心功能包括内置解析器处理常见数据类型、支持历史记录管理和过渡状态，以及在服务器组件中访问类型安全的查询参数。特别适合需要将应用状态与 URL 同步的应用场景，比如过滤和排序页面、多步骤表单等，能够显著提升用户体验和开发效率。",2,"2026-06-11 03:33:50","high_star"]