É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-url

useUrlState — en direct avec next.js

Tapez ci-dessous — regardez l'URL s'allumer

Premier composant client
Autre composant client

Lit depuis l'URL — sans props, sans context, les types et la structure sont préservés

{
name: string
age: undefinedundefined
agree_to_terms: falseboolean
tags: []
}

La même API, trois routeurs

Démarrage rapide

1. Définissez l’état
state
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 } }[];
};
2. Enveloppez-le dans un hook réutilisable
useFormState
'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 });
3. Utilisez-le dans n’importe quel composant — tous le partagent
ComponentA
'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>
    </>
};
ComponentB
'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>
};
4. Faites grandir le hook quand il en faut plus
useFormState - extended
'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 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 install

Exé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 vite
  • nextjs-ssrApp Router : transfert de searchParams, Proxy pour les layouts
  • react-router-remix-setupConfiguration de React Router v6/v7 ou Remix v2
  • form-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.

Quoistate-in-urlnuqs
Mise en placeAucune — importez le hook et c’est partiUn composant adaptateur enveloppe l’application
Forme de l’étatUn objet typé, comme React.useStateValeurs par clé, un parseur déclaré pour chacune
Réutilisation entre composantsEnveloppez le hook une fois — chaque composant partage l’état, sans propsVous extrayez votre propre hook autour de la table de parseurs
Objets et tableaux imbriquésIntégré — structure et types préservésParseur JSON plus votre propre validateur
DatesPréservées automatiquementParseur intégré, déclaré par clé
Taille, import complet~2,9 Ko gzip~6,7 Ko gzip
Dépendances au runtimeAucuneUne
RouteursNext.js, React Router v6/v7, Remix, helpers pour JS purNext.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

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

Uneed Embed Badge