| name | input-handling |
| description | Reconcile instant local input feedback with URL write throttling. setState updates internal state synchronously (instant re-render); setUrl is throttled and asynchronous (URL catches up on the next tick). Use this skill for text inputs, search boxes, sliders, range pickers, and any control that fires many updates per second where binding setUrl directly to onChange causes perceived lag or wasted URL writes.
|
| type | core |
| library | state-in-url |
| library_version | 6.1.3 |
| requires | ["feature-state-hook"] |
| sources | ["asmyshlyaev177/state-in-url:packages/urlstate/useUrlStateBase/useUrlStateBase.ts","asmyshlyaev177/state-in-url:packages/urlstate/utils.ts","asmyshlyaev177/state-in-url:README.md#update-state-only-and-sync-to-url-manually"] |
This skill builds on state-in-url/feature-state-hook. Read it first for the module-scoped default-state rule.
state-in-url — Input handling
useUrlState returns three setters with different timing:
| Setter | What updates | When |
|---|
setState(value) | Internal state only | Synchronous |
setUrl(value) | Internal state + URL | State sync, URL on next tick (throttled) |
setUrl() | Flush current state to URL | URL on next tick (diff-checked, no-op if equal) |
State updates always render immediately. URL writes are coalesced through an internal global timer (TIMEOUT constant in useUrlStateBase.ts) so a burst of setUrl calls produces one URL update.
Setup
export type SearchState = { q: string; sort: 'name' | 'date' };
export const SEARCH_STATE: SearchState = { q: '', sort: 'name' };
'use client';
import { useSearchParams } from 'next/navigation';
import { useUrlState } from 'state-in-url/next';
import { SEARCH_STATE } from './searchState';
export function useSearchState() {
const searchParams = useSearchParams();
return useUrlState(SEARCH_STATE, { searchParams });
}
Core Patterns
Instant input, deferred URL write (onBlur)
function SearchBox() {
const { urlState, setState, setUrl } = useSearchState();
return (
<input
value={urlState.q}
onChange={(e) => setState({ q: e.target.value })}
onBlur={() => setUrl()}
/>
);
}
setState updates state instantly (input feels native). setUrl() with no args flushes current state to the URL when the user finishes typing.
Discrete controls — setUrl directly
For click/select/toggle controls that fire at most a few times per second, skip the split. setUrl is already throttled.
<button onClick={() => setUrl({ sort: 'date' })}>Sort by date</button>
Common Mistakes
CRITICAL setUrl inside useEffect → infinite update loop
(Cross-skill failure — also in feature-state-hook.)
Wrong:
React.useEffect(() => {
setUrl({ q: urlState.q.trim() });
}, [urlState, setUrl]);
Correct:
React.useEffect(() => {
const trimmed = urlState.q.trim();
if (urlState.q !== trimmed) setUrl({ q: trimmed });
}, [urlState.q, setUrl]);
URL throttling does not break a state→effect→setUrl→state cycle.
Source: Maintainer interview
HIGH Binding setUrl directly to onChange of a typing input
Wrong:
<input
value={urlState.q}
onChange={(e) => setUrl({ q: e.target.value })}
/>
Correct:
<input
value={urlState.q}
onChange={(e) => setState({ q: e.target.value })}
onBlur={() => setUrl()}
/>
Every keystroke fires a URL update. The library coalesces them with an internal timer, but the perceived behavior is laggy URL with extra rerenders (one for state, another when URL settles). Issue #78 was filed exactly for this symptom — the throttling is intentional.
Source: GitHub issue #78 (asmyshlyaev177/state-in-url); README "Update state only and sync to URL manually"
MEDIUM Expecting window.location.search to reflect setUrl synchronously
Wrong:
setUrl({ tab: 'b' });
console.log(window.location.search);
Correct:
setUrl({ tab: 'b' });
setUrl is "last-write-wins" — it coalesces a burst of updates into one URL write on the next macrotask. Read urlState, not window.location.
Source: useUrlStateBase.ts (global timer); GitHub issue #78
See also
state-in-url/feature-state-hook — base pattern this skill builds on.
state-in-url/nextjs-ssr — for SSR-safe wiring of the search input on Next.js App Router.