État typé, vivant dans l'URL
useUrlState est l'état React qui s'écrit lui-même dans la chaîne de requête. Les objets, les tableaux et les dates conservent leurs types, chaque état est un lien partageable et survit aux rechargements — sans providers, sans code répétitif.
- ~2 KB en gzip
- zéro dépendance
- TypeScript-first
- Next.js / react-router / Remix
- MIT
npm i state-in-urluseUrlState — en direct avec next.js
Tapez ci-dessous — regardez l'URL s'allumer
Lit depuis l'URL — sans props, sans context, les types et la structure sont préservés
{name: stringage: undefinedundefinedagree_to_terms: falsebooleantags: []}
La même API, trois routeurs
Démarrage rapide
export const form: Form = {
name: '',
age: undefined,
agree_to_terms: false,
tags: [],
};
// use `Type` not `Interface`!
type Form = {
name: string;
age?: number;
agree_to_terms: boolean;
tags: { id: string; value: { text: string; time: Date } }[];
};'use client';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
export const ComponentA = () => {
// see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState
// useHistory: false to update sp on server component
const { urlState, setState, setUrl } = useUrlState(form, { useHistory: true });
return <>
<input
id="name"
value={urlState.name}
onChange={(ev) => setUrl({ name: ev.target.value })}
/>
// OR can update state immediately but sync change to url as needed
<input
value={urlState.name}
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
onBlur={() => setUrl()}
/>
<button onClick={() => setUrl((curr, initial) => initial)}>
Reset
</button>
</>
};'use client';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
// "searchParams" used to pass params from Server Components
export const ComponentB = ({ searchParams }: { searchParams?: object }) => {
const { urlState } = useUrlState(form, { searchParams });
// will be defaultValue from `form` if not in url, no need to check
return <div>name: {urlState.name}</div>
};'use client';
import React from 'react';
import { useUrlState } from 'state-in-url/next';
const form: Form={
name: '',
age: undefined,
agree_to_terms: false,
tags: [],
};
type Form = {
name: string;
age?: number;
agree_to_terms: boolean;
tags: {id: string; value: {text: string; time: Date } }[];
};
export const useFormState = ({ searchParams }: { searchParams?: object }) => {
const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
searchParams,
});
// first navigation will push new history entry
// all following will just replace that entry
// this way will have history with only 2 entries - ['/url', '/url?key=param']
const replace = React.useRef(false);
const setUrl = React.useCallback((
state: Parameters<typeof setUrlBase>[0],
opts?: Parameters<typeof setUrlBase>[1]
) => {
setUrlBase(state, { replace: replace.current, ...opts });
replace.current = true;
}, [setUrlBase]);
return { urlState, setUrl, resetUrl: reset };
};Vous utilisez un agent de codage IA ?
Les agents se trompent toujours sur les deux mêmes choses ici. Ils définissent la forme de l'état avec interface, que la contrainte JSONCompatible rejette d'emblée. Et ils construisent l'objet d'état par défaut dans le composant, ce qui casse le partage en silence — il est indexé par l'identité de l'objet, donc rien ne génère d'erreur, les deux composants cessent simplement de se voir.
Le paquet fournit donc six SKILL.md fichiers. Votre agent en charge un à la demande via TanStack Intent, et ils sont versionnés avec la bibliothèque plutôt qu'avec cette page.
npx @tanstack/intent@latest installExécutez une fois dans un projet qui a déjà state-in-url installé. Votre agent trouve alors les compétences dans node_modules/state-in-url/skills/.
feature-state-hookDéfinir l'état et envelopper useUrlState dans un hook à portée de fonctionnalitéinput-handlingChamps de texte, curseurs, tout ce qui change vitenextjs-ssrApp Router : transfert de searchParams, Proxy pour les layoutsreact-router-remix-setupConfiguration de React Router v6/v7 ou Remix v2form-library-integrationAssociation avec react-hook-form (ou formik)shared-state-no-urluseSharedState — partager sans toucher à l'URL
Les sources sont sur GitHub. Un agent qui ne peut pas charger les compétences d'Intent devrait lire llms.txt à la place — les mêmes règles, condensées en un seul fichier.
Pourquoi state-in-url ?
Il existe des bibliothèques d'état dans l'URL, mais la plupart sont soit fastidieuses à configurer, soit limitées dans ce qu'elles peuvent stocker. state-in-url vise à être celle qui fonctionne tout simplement : une API qui reflète React.useState, avec l'URL comme stockage.
Stockez l'état sans code répétitif, construisez des liens profonds et partagez des données entre composants client sans rapport — aucun provider nécessaire. La structure et les types sont préservés de bout en bout : un Date entre, une Date sort.
Construit en test-first, avec des suites unitaires et e2e inter-navigateurs qui s'exécutent à chaque commit.
Next.js : aucune limite Suspense
Le hook n'appelle jamais useSearchParams, donc un composant qui l'utilise n'a pas besoin d'être enveloppé dans Suspense et n'exclut pas sa page du pré-rendu — PPR et cacheComponents inclus. Il lit l'URL directement et suit chaque changement ultérieur, y compris un history.pushState issu d'un code qui n'en sait rien.
Pas sur Next.js ou react-router ?
Les helpers encodeState / decodeState fonctionnent avec n'importe quel framework ou du JS pur — les hooks sont une commodité par-dessus.
Consultez la page GitHub — une étoile fait beaucoup.
Partagez-le avec d'autres développeurs

