É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

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.

La même fonctionnalité, dans les deux

Un panneau de filtres : un texte de recherche, un numéro de page, une liste de tags et une date. nuqs déclare un parseur par clé et branche un adaptateur à la racine ; state-in-url prend l’objet et l’enveloppe dans un hook réutilisable.

app/layout.tsx (nuqs)
// app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <NuqsAdapter>{children}</NuqsAdapter>
      </body>
    </html>
  );
}
filters.tsx (nuqs)
'use client';
import {
  useQueryStates,
  parseAsString,
  parseAsInteger,
  parseAsArrayOf,
  parseAsIsoDateTime,
} from 'nuqs';

export const Filters = () => {
  const [filters, setFilters] = useQueryStates({
    q: parseAsString.withDefault(''),
    page: parseAsInteger.withDefault(1),
    tags: parseAsArrayOf(parseAsString).withDefault([]),
    since: parseAsIsoDateTime,
  });

  return (
    <input
      value={filters.q}
      onChange={(ev) => setFilters({ q: ev.target.value, page: 1 })}
    />
  );
};
filters.tsx (state-in-url)
'use client';
import { useUrlState } from 'state-in-url/next';

export const filters = {
  q: '',
  page: 1,
  tags: [] as string[],
  since: undefined as Date | undefined,
};

// One reusable hook = the whole API for this feature
export const useFilters = () => useUrlState(filters);

export const SearchBox = () => {
  const { urlState, setUrl } = useFilters();

  return (
    <input
      value={urlState.q}
      onChange={(ev) => setUrl({ q: ev.target.value, page: 1 })}
    />
  );
};

export const ActiveTags = () => {
  // Same state, another component - no props, no context
  const { urlState } = useFilters();

  return <>{urlState.tags.join(', ')}</>; // tags is still string[]
};

Ce seul hook personnalisé est toute l’API de la fonctionnalité : chaque composant qui l’appelle partage le même état typé — la liste de tags reste un tableau, la date revient en véritable objet Date. Pas de props, pas de context, pas de câblage par clé.

Mise en place et boilerplate

nuqs se branche sur votre routeur via un composant adaptateur enveloppant l’application, et chaque morceau d’état déclare son parseur. state-in-url fournit un hook par routeur — importez celui qui correspond, passez-lui un objet d’état par défaut, terminé. Rien n’enveloppe rien.

Next.js, SSR et prérendu

Sur l’App Router, state-in-url n’appelle jamais useSearchParams, donc les composants qui l’utilisent n’ont pas besoin d’une frontière Suspense et leurs pages restent prérendues — PPR compris. Les composants serveur lisent le même état via la prop searchParams, transmise telle quelle.

Migrer depuis nuqs

La migration est le plus souvent mécanique : rassemblez les clés d’une fonctionnalité dans un seul objet d’état par défaut, supprimez les déclarations de parseurs — des valeurs typées portent la même information — et remplacez les setters par clé par un seul setter acceptant un partiel. Chaque champ de premier niveau reste son propre paramètre de query.

Ce que valent les autres options

nuqs n’est pas la seule alternative. Le même besoin — un état typé dans la query string — est aussi couvert par les routeurs eux-mêmes et des bibliothèques plus anciennes, chacune avec son compromis.

BibliothèqueMise en placeObjets imbriqués et datesTailleÀ choisir quand
state-in-urlAucune — importez le hookPréservés automatiquement, types compris~2,9 Ko gzip, zéro dépendanceVous voulez un objet typé sans configuration sur Next.js, React Router ou Remix
nuqsComposant adaptateur, parseur par cléParseur JSON plus votre propre validateur~6,7 Ko gzip, une dépendanceVous voulez chaque valeur comme query param lisible
TanStack RoutervalidateSearch sur chaque routeJSON-first pour objets et tableaux ; dates via une sérialisation maisonIntégré au routeurVous êtes sur TanStack Router — utilisez ce qu’il fournit
use-query-paramsProvider plus adaptateur de routeur, config par paramètreVia un type de paramètre JSON, faiblement typé~4,4 Ko gzip plus serialize-query-paramsUne base de code déjà construite dessus
useSearchParamsAucune — intégré au routeurChaînes uniquement — parsing, types et défauts à votre charge0 KoUn ou deux paramètres plats, pas besoin de bibliothèque

Questions fréquentes

state-in-url est-il une bonne alternative à nuqs ?
Oui, quand vous voulez un objet typé complet dans l’URL sans configuration : pas de composant adaptateur, pas de parseurs par clé, objets imbriqués et dates préservés automatiquement. nuqs reste le meilleur choix pour un query param lisible par valeur, ou si vous êtes sur TanStack Router.
Lequel est le plus léger, state-in-url ou nuqs ?
Mesuré avec esbuild (minify + gzip, import de la bibliothèque entière) en août 2026 : state-in-url pèse ~2,9 Ko sans dépendance ; nuqs 2.10.1 pèse ~6,7 Ko avec une dépendance. Importer un sous-ensemble réduit les deux.
state-in-url a-t-il besoin d’un adaptateur ou d’un provider ?
Non. Chaque routeur a son point d’entrée — importez le hook correspondant, passez-lui un objet d’état par défaut, et ça fonctionne. Aucun composant adaptateur autour de l’application, aucun provider de contexte à configurer.
Est-il difficile de migrer de nuqs vers state-in-url ?
Le plus souvent non : rassemblez les clés d’une fonctionnalité dans un seul objet d’état par défaut, supprimez les déclarations de parseurs et remplacez les setters par clé par un seul setter acceptant un partiel. Chaque champ de premier niveau reste son propre paramètre de query.
Et les search params de TanStack Router ?
Si vous êtes sur TanStack Router, utilisez ce qu’il fournit : des search params JSON-first validés par route avec validateSearch. state-in-url et nuqs comptent quand votre routeur est Next.js, React Router ou Remix, sans search params typés intégrés.