{"content":"# URL State with nuqs\n\nSync React state to URL query parameters for shareable filters, search queries, and deep links to modal dialogs. Preserves UI state on browser back/forward navigation.\n\n## Installation\n\n```bash\nbun add nuqs\n```\n\n## Setup the Adapter\n\nWrap your app with the `NuqsAdapter` in your root layout:\n\n```tsx\nimport { NuqsAdapter } from \"nuqs/adapters/next/app\";\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <body>\n        <NuqsAdapter>{children}</NuqsAdapter>\n      </body>\n    </html>\n  );\n}\n```\n\n## Suspense Boundary Requirement\n\nnuqs uses `useSearchParams` behind the scenes, which requires a Suspense boundary. Without one, Next.js throws an error during static rendering.\n\nWrap nuqs-using components with Suspense via a wrapper component. This keeps the Suspense boundary colocated with the component that needs it, avoiding the need to add Suspense in every consuming component:\n\n```tsx\nimport { Suspense } from \"react\";\n\ntype SearchInputProps = {\n  placeholder?: string;\n};\n\n// Public component with built-in Suspense\nexport function SearchInput(props: SearchInputProps) {\n  return (\n    <Suspense fallback={<input placeholder={props.placeholder} disabled />}>\n      <SearchInputClient {...props} />\n    </Suspense>\n  );\n}\n```\n\n```tsx\n\"use client\";\n\nimport { useQueryState, parseAsString } from \"nuqs\";\n\n// Internal client component that uses nuqs\nfunction SearchInputClient({ placeholder = \"Search...\" }: SearchInputProps) {\n  const [search, setSearch] = useQueryState(\"q\", parseAsString.withDefault(\"\"));\n\n  function handleChange(value: string) {\n    setSearch(value || null);\n  }\n\n  return (\n    <input\n      value={search}\n      onChange={(e) => handleChange(e.target.value)}\n      placeholder={placeholder}\n    />\n  );\n}\n```\n\nNow `SearchInput` can be used anywhere without the consumer needing to add Suspense:\n\n```tsx\n// No Suspense needed here - it's built into SearchInput\n<SearchInput placeholder=\"Search recipes...\" />\n```\n\n## Basic Usage\n\nReplace `useState` with `useQueryState` to sync state to the URL. This creates URLs like `?q=react` when searching.\n\n## Parsers\n\nnuqs provides parsers for different data types:\n\n```tsx\nimport {\n  useQueryState,\n  parseAsString,\n  parseAsBoolean,\n  parseAsArrayOf,\n} from \"nuqs\";\n\n// String with default\nconst [search, setSearch] = useQueryState(\"q\", parseAsString.withDefault(\"\"));\n\n// Boolean (for toggles)\nconst [showArchived, setShowArchived] = useQueryState(\n  \"archived\",\n  parseAsBoolean.withDefault(false),\n);\n\n// Array of strings (for multi-select filters)\nconst [tags, setTags] = useQueryState(\n  \"tags\",\n  parseAsArrayOf(parseAsString).withDefault([]),\n);\n```\n\n## Deep Links to Modals\n\nUse query params to control modal visibility, enabling shareable links. Apply the wrapper pattern to keep Suspense colocated:\n\n```tsx\nimport { Suspense } from \"react\";\n\ntype SettingsDialogProps = {\n  children: React.ReactNode;\n};\n\n// Public component with built-in Suspense\nexport function SettingsDialog(props: SettingsDialogProps) {\n  return (\n    <Suspense fallback={<span>{props.children}</span>}>\n      <SettingsDialogClient {...props} />\n    </Suspense>\n  );\n}\n```\n\n```tsx\n\"use client\";\n\nimport { useQueryState, parseAsBoolean } from \"nuqs\";\nimport { Dialog, DialogTrigger, DialogContent } from \"@/components/ui/dialog\";\n\nfunction SettingsDialogClient({ children }: SettingsDialogProps) {\n  const [isOpen, setIsOpen] = useQueryState(\n    \"settings\",\n    parseAsBoolean.withDefault(false),\n  );\n\n  return (\n    <Dialog open={isOpen} onOpenChange={(open) => setIsOpen(open || null)}>\n      <DialogTrigger asChild>{children}</DialogTrigger>\n      <DialogContent>{/* Settings content */}</DialogContent>\n    </Dialog>\n  );\n}\n```\n\nLink directly to the open modal with `?settings=true`.\n\nFor modals that operate on specific items (like delete confirmations), use the item ID:\n\n```tsx\nconst [deleteId, setDeleteId] = useQueryState(\"delete\", parseAsString);\n\n// Open: setDeleteId(\"item-123\")\n// Close: setDeleteId(null)\n// Deep link: ?delete=item-123\n\n<AlertDialog\n  open={!!deleteId}\n  onOpenChange={(open) => !open && setDeleteId(null)}\n>\n```\n\n## SEO: Canonical URLs\n\nSince query parameters are for local UI state, add a canonical URL to tell search engines to index the page without them:\n\n```tsx\nimport type { Metadata } from \"next\";\n\nexport const metadata: Metadata = {\n  alternates: {\n    canonical: \"/\",\n  },\n};\n```\n\nThis prevents duplicate content issues from filter variations like `?q=react` and `?q=nextjs` being indexed separately.\n\n## Clearing State\n\nSetting a value to `null` removes it from the URL:\n\n```tsx\n// Clear single param\nsetSearch(null);\n\n// Clear multiple params\nfunction clearFilters() {\n  setSearch(null);\n  setTags(null);\n  setShowArchived(null);\n}\n```\n\nWhen using `.withDefault()`, setting to `null` clears the URL param but returns the default value as state."}