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