| name | react-url-state-management |
| description | Design and implement URL-based state in React applications using search params/query params instead of local-only component state when state must be shareable, bookmarkable, reload-persistent, and synchronized with browser back/forward navigation. Use when users mention URL state, search params, query strings, deep links, filters/sort/pagination in the URL, React Router useSearchParams, Next.js useSearchParams, or replacing local state with URL-driven state. |
React URL State Management
Use this skill to model UI state in the URL (usually search params) as a first-class state container.
Why URL state
Use URL state when state should be:
- shareable via link
- bookmarkable
- preserved across refresh
- navigable with browser back/forward
- inspectable for debugging
- suitable for server-side data fetching keyed by params
Typical URL-state fields:
- active tab
- search text
- sort and filter selections
- pagination cursor/page
- onboarding step
- sidebar/modal open state (when shareable)
State concern split (important)
Model state by concern before choosing tools:
- Remote state (server data/cache): use data-fetching libraries.
- URL state (navigation/shareable UI): use query/path state.
- Local state (ephemeral component UI): use component state.
- Shared in-memory state (cross-tree app behavior): use context/store only when needed.
Do not force all concerns into one global store.
When not to use URL state
Keep state local when it is:
- sensitive or secret
- very high-frequency transient UI state (for example drag position per frame)
- too large or deeply nested for practical query serialization
- purely ephemeral visual state (for example hover/open animation progress)
Inputs to gather first
- route/path where state lives
- fields that should be in URL (search, sort, filters, page, tab)
- data types and defaults for each field
- invalid-value behavior (coerce, fallback, or clear)
- history behavior (
push vs replace)
- desired canonical URL format
Implementation workflow
- Define a URL schema.
- Parse search params into typed application state.
- Apply defaults for missing params.
- Validate and normalize invalid values.
- Derive UI from parsed state, not duplicated local state.
- Update URL from user actions with explicit
push/replace rules.
- Keep update functions atomic so multi-field changes happen in one URL write.
- Ensure serialization is stable to avoid URL churn and noisy history.
- Prevent loops by separating read-from-URL and write-to-URL phases.
- Add tests for deep-linking, refresh persistence, and back/forward behavior.
Push vs replace policy
Use push for user-meaningful navigation events:
- changing page number
- applying a new filter set that users may want to step back through
Use replace for high-frequency updates:
- typing in search box with debounce
- intermediate slider updates
Framework patterns
React Router
- Use
useSearchParams for read/write.
- Keep parser/serializer in one utility module.
- Prefer one function that merges updates into current params.
const [params, setParams] = useSearchParams();
const page = Number(params.get("page") ?? 1);
setParams((prev) => {
prev.set("page", String(page + 1));
return prev;
}, { replace: false });
Next.js App Router
- Read with
useSearchParams.
- Update with
useRouter + usePathname.
- Construct next params via
URLSearchParams before routing.
- Remember:
useSearchParams is read-only and client-only.
- For prerendered routes, place search-param readers under a
Suspense boundary.
- If server-side data depends on search params, prefer the Page
searchParams prop and pass values down.
- Avoid manual two-way sync between local state and URL for non-trivial flows; it is error-prone.
const router = useRouter();
const pathname = usePathname();
const params = new URLSearchParams(searchParams);
params.set("tab", "activity");
router.replace(`${pathname}?${params.toString()}`);
TanStack Router
- Define and validate search param schema at route level.
- Prefer route-level typed search for stronger contracts.
nuqs (recommended for React URL state ergonomics)
- Use
useQueryState/useQueryStates for useState-like URL state APIs.
- Use built-in parsers (
parseAsInteger, booleans, dates, etc.) and custom parsers for domain types.
- Prefer batched multi-key updates for related filters to avoid intermediate inconsistent URLs.
- Choose history mode intentionally (
replace for noisy updates, append/push for meaningful navigation steps).
- In Next.js, decide when server should re-render from URL updates; use shallow/client-first defaults only when server sync is not required.
- Prefer parser defaults to keep URL parsing typed and predictable (for example default tab/page values).
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
const [{ page, q }, setQuery] = useQueryStates({
page: parseAsInteger.withDefault(1),
q: parseAsString.withDefault(""),
});
setQuery({ q: "laptop" }, { history: "replace" });
setQuery({ page: 2 }, { history: "push" });
Pros and tradeoffs
Benefits:
- better shareability and collaboration
- durable UI state across sessions
- easier reproduction of bugs
- stronger integration with route-aware data loading
- better deep-linking for tabs/modals/results pages
- more crawlable and canonical page states for discoverability-sensitive routes
Costs:
- extra parsing/serialization complexity
- schema/versioning responsibility
- URL length and readability constraints
- careful history management required
Common failure modes
- Duplicating source of truth in both local state and URL.
Fix: derive local view state from parsed URL state whenever possible.
- Writing partial params and unintentionally dropping others.
Fix: always merge with current params before writing.
- Invalid param values crashing UI.
Fix: validate and coerce to defaults.
- Overusing
push and polluting history.
Fix: switch noisy updates to replace.
- Allowing multiple URL shapes for the same logical state.
Fix: canonicalize key order/default omission and normalize values before writing.
- Encoding complex objects inconsistently across screens.
Fix: centralize parser/serializer logic and reuse the same schema module.
- Route-as-modal state not encoded, making dialogs unshareable.
Fix: model modal identity in path or search params when shareability matters.
Completion checklist
- URL is the canonical source for chosen fields.
- Deep link opens correct UI state.
- Refresh preserves state.
- Back/forward behaves predictably.
- Invalid params fail safely.
push vs replace policy is documented in code.
- Canonical URL normalization is applied consistently.
- Modal/tab states that should be shareable are encoded in URL.