É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 / Astro
  • MIT
npm i state-in-url

useUrlState — en direct avec astro

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, quatre frameworks

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
import { useUrlState } from 'state-in-url/astro';
import { form } from './form';

// One hook per feature - the whole API for this state.
// searchParams is the island prop: with it the server render matches the URL
export const useFormState = (searchParams?: Record<string, string>) =>
  useUrlState(form, { searchParams });
3. Utilisez-le dans n’importe quel composant — tous le partagent
index.astro
---
import { ComponentA } from '../components/ComponentA';
import { ComponentB } from '../components/ComponentB';

// A plain object: island props are serialized, URLSearchParams is not
const searchParams = Object.fromEntries(Astro.url.searchParams);
---

<ComponentA client:load searchParams={searchParams} />
<ComponentB client:load searchParams={searchParams} />
ComponentA
import { useFormState } from './useFormState';

export const ComponentA = ({ searchParams }: { searchParams?: Record<string, string> }) => {
  // see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/astro/useUrlState
  const { urlState, setUrl, setState } = useFormState(searchParams);

  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
import { useFormState } from './useFormState';

export const ComponentB = ({ searchParams }: { searchParams?: Record<string, string> }) => {
  // same state as ComponentA - a separate island, no props between them, 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
import React from 'react';
import { useUrlState } from 'state-in-url/astro';
import { form } from './form';

export const useFormState = (searchParams?: Record<string, string>) => {
  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 };
};

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 placeNext.js, React Router v6/v7, Remix, Astro, helpers pour JS purUn 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