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
64
77%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.agents/skills/you-might-not-need-url-state/SKILL.mdArguments:
User arguments: $ARGUMENTS
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:
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.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.
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-outparseAsString/parseAsInteger/parseAsBoolean/parseAsStringLiteral/parseAsArrayOf/createParser)withDefault, history, shallow, clearOnDefaultcreateSearchParamsCache for server readsuseSearchParams().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.)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.popstate listener). The URL is the single source of truth; derive from it, don't mirror it.TableDefinition/SkillDefinition/etc. Store the id and derive the object from the loaded list (items.find(i => i.id === id)).useDebouncedSearchSetter (never a local useState mirror + reconcile effect, and never inline limitUrlUpdates wiring); keep canvas/presence/resize state in Zustand.useState: a tab/filter/sort/pagination/selected-entity that should be a link but lives in local state. Migrate it to the URL.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.SORT_DIRECTIONS/default-sort constants/activeSort derivation instead of createSortParams + useUrlSort, or an inline debounced-search setter instead of useDebouncedSearchSetter/useSettingsSearch..claude/rules/sim-url-state.md and the nuqs docs above to understand the guidelinessearch-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.6f514c1
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.