État typé dans l’URL pour React et Next.js — comme useState
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, il survit aux rechargements et le bouton Précédent fonctionne — sans providers, sans frontière Suspense, sans code répétitif.
- ~2 KB en gzip
- zéro dépendance
- TypeScript-first
- Next.js / react-router / Remix / Astro
- 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: []}
Gestion d’état dans l’URL pour Next.js, React Router, Remix et Astro — la même API
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';
// One hook per feature - the whole API for this state
// "searchParams" only needed to pass params from Server Components
export const useFormState = (searchParams?: object) =>
useUrlState(form, { searchParams });'use client';
import { useFormState } from './useFormState';
export const ComponentA = () => {
// see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState
const { urlState, setState, setUrl } = useFormState();
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 { useFormState } from './useFormState';
// "searchParams" used to pass params from Server Components
export const ComponentB = ({ searchParams }: { searchParams?: object }) => {
// same state as ComponentA - no props, no context
const { urlState } = useFormState(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';
import { form } from './form';
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 sept 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 v2astro-setupÎlots Astro (React ou Preact), ou pages sans framework côté clientform-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.
state-in-url vs nuqs
Vous cherchez une alternative à nuqs ? Les deux gardent un état typé dans la query string ; elles diffèrent par la configuration requise et par ce qu’une valeur peut être.
| Quoi | state-in-url | nuqs |
|---|---|---|
| Mise en place | Next.js, React Router v6/v7, Remix, Astro, helpers pour JS pur | Un composant adaptateur enveloppe l’application |
| Forme de l’état | Un objet typé, comme React.useState | Valeurs par clé, un parseur déclaré pour chacune |
| Réutilisation entre composants | Enveloppez le hook une fois — chaque composant partage l’état, sans props | Vous extrayez votre propre hook autour de la table de parseurs |
| Objets et tableaux imbriqués | Intégré — structure et types préservés | Parseur JSON plus votre propre validateur |
| Dates | Préservées automatiquement | Parseur intégré, déclaré par clé |
| Taille, import complet | ~2,9 Ko gzip | ~6,7 Ko gzip |
| Dépendances au runtime | Aucune | Une |
| Routeurs | Next.js, React Router v6/v7, Remix, helpers pour JS pur | Next.js, React Router, Remix, TanStack Router, React pur |
Tailles : import de la bibliothèque entière, esbuild minify + gzip, mesuré en août 2026 face à nuqs 2.10.1.
nuqs est une bonne bibliothèque — choisissez-la pour un query param lisible par valeur, ou si vous êtes sur TanStack Router. Choisissez state-in-url pour un objet typé complet dans l’URL, sans configuration.
Lisez la comparaison complète — la même fonctionnalité dans les deux, et comment migrer
État dans l’URL en React — questions fréquentes
- Pourquoi garder l’état React dans l’URL ?
- Une URL qui porte l’état est un lien partageable : rechargez-la, mettez-la en favori ou envoyez-la, et les mêmes filtres, le même onglet ou la même page s’ouvrent. Précédent et Suivant fonctionnent sans rien faire, et des composants sans rapport lisent les mêmes valeurs sans provider. state-in-url le fait avec un seul objet typé plutôt qu’avec des chaînes parsées à la main.
- Quel état a sa place dans l’URL ?
- Tout ce qu’un lecteur pourrait mettre en favori ou partager : filtres, tri, pagination, onglet actif, plage de dates, texte de recherche. Laissez de côté ce qui est privé, énorme ou purement transitoire — jetons d’authentification, ouverture d’une boîte de dialogue, position de la souris. Un test rapide : un lien partagé aurait-il encore un sens avec cette valeur dedans ?
- Comment lire et écrire des paramètres d’URL en React avec state-in-url ?
- Appelez useUrlState avec un objet d’état par défaut. urlState contient les valeurs courantes, déjà typées ; setUrl écrit un objet partiel dans la chaîne de requête ; setState met l’état à jour sans toucher à l’URL tant que vous ne l’y écrivez pas. Nombres, booléens, tableaux, objets imbriqués et Dates reviennent avec les types qu’ils avaient en entrant.
- L’état dans l’URL survit-il à un rechargement de page ?
- Oui. L’état est la chaîne de requête, donc un rechargement, un favori ou un lien collé ailleurs le restaure. Sur l’App Router de Next.js, passez la prop searchParams de la page au hook pour que le premier rendu serveur affiche déjà les bonnes valeurs plutôt que les valeurs par défaut.
- Fonctionne-t-il avec les Server Components de Next.js, sans frontière Suspense ?
- Oui. Le hook n’appelle jamais useSearchParams, donc un composant qui l’utilise n’a pas besoin de frontière Suspense et n’exclut pas la page du prérendu, PPR compris. Les Server Components lisent le même état via la prop searchParams ; un layout peut le décoder depuis un en-tête défini dans proxy.ts.
- Puis-je synchroniser react-hook-form ou une bibliothèque de tableaux avec l’URL ?
- Oui. Gardez la bibliothèque de formulaires comme source de vérité, initialisez-la avec urlState comme valeurs par défaut, et reflétez ses changements avec setUrl depuis un gestionnaire de changement ou un effet. Le même schéma fonctionne pour l’état de TanStack Table, les panneaux de filtres et tout ce qui expose des valeurs et un setter.
- Quels frameworks state-in-url prend-il en charge ?
- Next.js 14 à 16 avec l’App Router, React Router v6 et v7, Remix v2 et les îlots Astro (React ou Preact), chacun via son propre point d’entrée. Le JavaScript pur et tout autre framework peuvent utiliser directement les helpers encodeState et decodeState. La bibliothèque pèse ~2 KB en gzip, sans dépendance.
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 frontière 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

