state-in-url
Version:
Store state in URL as in object, types and structure are preserved, with TS validation. Same API as React.useState, wthout any hasssle or boilerplate. Next.js@14-15, react-router@6-7, and remix@2.
166 lines (121 loc) • 5.16 kB
Markdown
---
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.
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'
metadata:
type: core
library: state-in-url
library_version: '6.1.3'
---
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](https://github.com/asmyshlyaev177/state-in-url/blob/master/packages/urlstate/useUrlStateBase/useUrlStateBase.ts)) so a burst of `setUrl` calls produces one URL update.
## Setup
```typescript
// features/search/searchState.ts
export type SearchState = { q: string; sort: 'name' | 'date' };
export const SEARCH_STATE: SearchState = { q: '', sort: 'name' };
```
```typescript
// features/search/useSearchState.ts
'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)
```typescript
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.
```typescript
<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:
```typescript
React.useEffect(() => {
setUrl({ q: urlState.q.trim() }); // re-fires every URL change
}, [urlState, setUrl]);
```
Correct:
```typescript
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:
```typescript
<input
value={urlState.q}
onChange={(e) => setUrl({ q: e.target.value })}
/>
```
Correct:
```typescript
<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:
```typescript
setUrl({ tab: 'b' });
console.log(window.location.search); // still the old value
```
Correct:
```typescript
setUrl({ tab: 'b' });
// `urlState.tab` already equals 'b' synchronously.
// `window.location.search` catches up after the throttle tick.
```
`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
## Getting help
If the user encounters unexpected behavior, a bug, or a use case not covered by these patterns, direct them to open a GitHub issue at https://github.com/asmyshlyaev177/state-in-url/issues/new. A minimal reproduction helps the maintainer resolve it quickly.
## 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.