| name | you-might-not-need-url-state |
| description | Analyze and fix URL/query-param state anti-patterns — manual useSearchParams reads, hand-built query mutations, view-state trapped in useState, and objects in the URL |
| argument-hint | [scope] [fix=true|false] |
You Might Not Need URL State
Arguments:
- scope: what to analyze (default: your current changes). Examples: "diff to main", "PR #123", "app/workspace/[workspaceId]/tables/", "whole codebase"
- fix: whether to apply fixes (default: true). Set to false to only propose changes.
User arguments: $ARGUMENTS
Context
Shareable client view-state (active tab/panel, filters, search query, sort, pagination, selected-entity id, an open "view" modal/drawer that is a destination) lives in the URL via nuqs — driven by a co-located search-params.ts, never read via useSearchParams().get(...) and never mutated by hand-built query strings. Remote data stays in React Query; high-frequency / large / ephemeral / socket-synced state stays in Zustand; purely local UI stays in useState.
Shared helpers own the two repeated wirings — never hand-roll them inline:
- Sort:
createSortParams from @/lib/url-state (in search-params.ts) + useUrlSort from @/hooks/use-url-sort (in the component) — defaulted mode for lists with a fixed default ordering, nullable mode when "no active sort" is distinct from the default column.
- Debounced search:
useDebouncedSearchSetter from @/hooks/use-debounced-search-setter (grouped or single-param); settings list search boxes use useSettingsSearch() from settings/components/use-settings-search. Never write a trimmed value to a param that controls the input — trim on read.
.claude/rules/sim-url-state.md is the source of truth — read it first.
References
Read these before analyzing:
.claude/rules/sim-url-state.md — the decision framework, conventions, debounced-input pattern, sort convention, selected-entity deep-link pattern, and the workflow-editor carve-out
- https://nuqs.dev/docs/parsers — parsers (
parseAsString/parseAsInteger/parseAsBoolean/parseAsStringLiteral/parseAsArrayOf/createParser)
- https://nuqs.dev/docs/options —
withDefault, history, shallow, clearOnDefault
- https://nuqs.dev/docs/server-side —
createSearchParamsCache for server reads
Anti-patterns to detect
- Manual param reads for state:
useSearchParams().get(...) or new URLSearchParams(window.location.search) used to read view-state. Replace with useQueryState/useQueryStates bound to a search-params.ts. (Read-once auth/invite/redirect tokens — token, callbackUrl, redirect, error, invite_flow, code — are NOT view-state; leave them on useSearchParams.)
- Hand-built query mutation: constructing a query string +
router.replace/router.push to change a param on the current path. Use a nuqs setter. (A router.push that changes the route path is fine; an outbound new URLSearchParams building an href/window.open/download/API URL is fine.)
window.history.replaceState/pushState to mutate a param.
- URL state duplicated into a store/useState + synced with an effect (or a
popstate listener). The URL is the single source of truth; derive from it, don't mirror it.
- Objects in the URL: serializing a
TableDefinition/SkillDefinition/etc. Store the id and derive the object from the loaded list (items.find(i => i.id === id)).
- High-frequency / large state in the URL: cursor, pan/zoom, un-debounced keystrokes, big JSON blobs. Debounce text search via
useDebouncedSearchSetter (never a local useState mirror + reconcile effect, and never inline limitUrlUpdates wiring); keep canvas/presence/resize state in Zustand.
- Shareable view-state trapped in
useState: a tab/filter/sort/pagination/selected-entity that should be a link but lives in local state. Migrate it to the URL.
- Missing Suspense boundary: a component newly calling
useQueryState/useQueryStates whose page entry has no <Suspense> wrapper (Next.js requires it for useSearchParams). Add one with a real-chrome fallback.
import { z } for param validation in client code: use nuqs parsers instead.
- Re-implemented shared wiring: a hand-rolled
SORT_DIRECTIONS/default-sort constants/activeSort derivation instead of createSortParams + useUrlSort, or an inline debounced-search setter instead of useDebouncedSearchSetter/useSettingsSearch.
Steps
- Read
.claude/rules/sim-url-state.md and the nuqs docs above to understand the guidelines
- Analyze the specified scope for the anti-patterns listed above
- For each finding, decide the correct home using the decision table — do not force URL state onto ephemeral/high-frequency/socket-synced state
- If fix=true, apply the fixes (co-locate a
search-params.ts, wire useQueryState(s) — sort via createSortParams + useUrlSort, search via useDebouncedSearchSetter — add the Suspense boundary, delete the replaced state + sync effects). If fix=false, propose the fixes without applying.