you-might-not-need-url-state
GitHub分析并修复前端 URL 状态管理反模式,强制使用 nuqs 库替代手动 useSearchParams 读取、手写查询字符串拼接及 useState 同步,确保 URL 作为视图状态单一事实来源。
Trigger Scenarios
Install
npx skills add simstudioai/sim --skill you-might-not-need-url-state -g -y
SKILL.md
Frontmatter
{
"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:
createSortParamsfrom@/lib/url-state(insearch-params.ts) +useUrlSortfrom@/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:
useDebouncedSearchSetterfrom@/hooks/use-debounced-search-setter(grouped or single-param); settings list search boxes useuseSettingsSearch()fromsettings/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 —
createSearchParamsCachefor server reads
Anti-patterns to detect
- Manual param reads for state:
useSearchParams().get(...)ornew URLSearchParams(window.location.search)used to read view-state. Replace withuseQueryState/useQueryStatesbound to asearch-params.ts. (Read-once auth/invite/redirect tokens —token,callbackUrl,redirect,error,invite_flow,code— are NOT view-state; leave them onuseSearchParams.) - Hand-built query mutation: constructing a query string +
router.replace/router.pushto change a param on the current path. Use a nuqs setter. (Arouter.pushthat changes the route path is fine; an outboundnew URLSearchParamsbuilding anhref/window.open/download/API URL is fine.) window.history.replaceState/pushStateto mutate a param.- URL state duplicated into a store/useState + synced with an effect (or a
popstatelistener). 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 localuseStatemirror + reconcile effect, and never inlinelimitUrlUpdateswiring); 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/useQueryStateswhose page entry has no<Suspense>wrapper (Next.js requires it foruseSearchParams). 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/activeSortderivation instead ofcreateSortParams+useUrlSort, or an inline debounced-search setter instead ofuseDebouncedSearchSetter/useSettingsSearch.
Steps
- Read
.claude/rules/sim-url-state.mdand 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, wireuseQueryState(s)— sort viacreateSortParams+useUrlSort, search viauseDebouncedSearchSetter— add the Suspense boundary, delete the replaced state + sync effects). If fix=false, propose the fixes without applying.
Version History
- ceda457 Current 2026-08-20 15:30


