From andrelandgraf-fullstackrecipes
Sync React state to URL query params with nuqs: Suspense wrappers, parsers, clearing, and deep-linkable dialogs. For shareable filters, search, or URL-driven dialogs.
How this skill is triggered — by the user, by Claude, or both
Slash command
/andrelandgraf-fullstackrecipes:url-state-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Sync React state to URL query params with nuqs.
Sync React state to URL query params with nuqs.
Complete these setup recipes first:
nuqs reads useSearchParams, so it needs a Suspense boundary. Colocate it by exporting a public wrapper that suspends an internal client component — consumers then use the component without adding Suspense themselves.
import { Suspense } from "react";
type SearchInputProps = { placeholder?: string };
export function SearchInput(props: SearchInputProps) {
return (
<Suspense fallback={<input placeholder={props.placeholder} disabled />}>
<SearchInputClient {...props} />
</Suspense>
);
}
"use client";
import { useQueryState, parseAsString } from "nuqs";
function SearchInputClient({ placeholder = "Search..." }: SearchInputProps) {
const [search, setSearch] = useQueryState("q", parseAsString.withDefault(""));
return (
<input
value={search}
onChange={(e) => setSearch(e.target.value || null)}
placeholder={placeholder}
/>
);
}
Replace useState with useQueryState plus a parser. Use .withDefault() to read a fallback while keeping the URL clean.
"use client";
import {
useQueryState,
parseAsString,
parseAsBoolean,
parseAsArrayOf,
} from "nuqs";
const [search, setSearch] = useQueryState("q", parseAsString.withDefault(""));
const [showArchived, setShowArchived] = useQueryState(
"archived",
parseAsBoolean.withDefault(false),
);
const [tags, setTags] = useQueryState(
"tags",
parseAsArrayOf(parseAsString).withDefault([]),
);
Set a value to null to remove it from the URL. With .withDefault(), the param clears but reads return the default.
setSearch(null);
function clearFilters() {
setSearch(null);
setTags(null);
setShowArchived(null);
}
Drive dialog visibility from a URL param so it's shareable and survives back/forward. Wrap in the same Suspense pattern.
import { Suspense } from "react";
type DeleteDialogProps = { onDelete: (id: string) => Promise<void> };
export function DeleteDialog(props: DeleteDialogProps) {
return (
<Suspense fallback={null}>
<DeleteDialogClient {...props} />
</Suspense>
);
}
"use client";
import { useQueryState, parseAsString } from "nuqs";
import { AlertDialog, AlertDialogContent } from "@/components/ui/alert-dialog";
function DeleteDialogClient({ onDelete }: DeleteDialogProps) {
const [deleteId, setDeleteId] = useQueryState("delete", parseAsString);
async function handleDelete() {
if (!deleteId) return;
await onDelete(deleteId);
setDeleteId(null);
}
return (
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
<AlertDialogContent>
<Button onClick={handleDelete}>Delete</Button>
</AlertDialogContent>
</AlertDialog>
);
}
Open it from anywhere by setting the param — setDeleteId("item-123") yields the deep link /items?delete=item-123.
function ItemRow({ item }: { item: Item }) {
const [, setDeleteId] = useQueryState("delete", parseAsString);
return (
<Button variant="ghost" onClick={() => setDeleteId(item.id)}>
Delete
</Button>
);
}
npx claudepluginhub joshuarweaver/cascade-code-general-misc-3 --plugin andrelandgraf-fullstackrecipesProvides best practices for nuqs type-safe URL query state management in Next.js and React frameworks, covering parsers, setup, server integration, and performance.
Provides React hooks reference with syntax, patterns for useState/useReducer state, useEffect side effects/cleanup, useRef/useContext, useMemo/useCallback optimization, React 19 hooks, custom hooks, and best practices.
Guides on modern React state management with Redux Toolkit, Zustand, Jotai, and React Query. Use for global state, server state, or choosing a solution.