Estado tipado, viviendo en la URL

useUrlState es el estado de React que se escribe a sí mismo en la cadena de consulta. Los objetos, los arrays y las fechas conservan sus tipos, cada estado es un enlace compartible y sobrevive a las recargas, sin providers ni código repetitivo.

  • ~2 KB en gzip
  • cero dependencias
  • TypeScript-first
  • Next.js / react-router / Remix
  • MIT
npm i state-in-url

useUrlState — en vivo con remix.js

Escribe abajo — observa cómo se enciende la URL

Primer componente de cliente
Otro componente de cliente

Lee desde la URL — sin props, sin context, los tipos y la estructura se conservan

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

La misma API, tres routers

Inicio rápido

1. Define el estado
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. Envuélvelo en un hook reutilizable
useFormState
import { useUrlState } from 'state-in-url/remix';
import { form } from './form';

// One hook per feature - the whole API for this state
export const useFormState = () => useUrlState(form);
3. Úsalo en cualquier componente — todos lo comparten
ComponentA
import { useFormState } from './useFormState';

export const ComponentA = () => {
  // see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/remix/useUrlState
  const { urlState, setUrl, setState } = 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
import { useFormState } from './useFormState';

export const ComponentB = () => {
  // same state as ComponentA - no props, no context
  const { urlState } = useFormState();

// will be defaultValue from `form` if not in url, no need to check

  return <div>name: {urlState.name}</div>
};
4. Haz crecer el hook cuando necesites más
useFormState - extended
import React from 'react';
import { useUrlState } from 'state-in-url/remix';
import { form } from './form';

export const useFormState = () => {
  const { urlState, setUrl: setUrlBase, reset } = useUrlState(form);

  // 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

¿Buscas una alternativa a nuqs? Ambas guardan estado tipado en la query string; difieren en cuánto hay que configurar y en qué puede ser un valor.

Quéstate-in-urlnuqs
ConfiguraciónNinguna — importa el hook y listoUn componente adaptador envuelve la app
Forma del estadoUn objeto tipado, como React.useStateValores por clave, con un parser declarado para cada una
Reutilización entre componentesEnvuelve el hook una vez — cada componente comparte el estado, sin propsExtraes tu propio hook alrededor del mapa de parsers
Objetos y arrays anidadosIntegrado — estructura y tipos se conservanParser JSON más tu propio validador
FechasSe conservan automáticamenteParser integrado, declarado por clave
Tamaño, import completo~2,9 KB gzip~6,7 KB gzip
Dependencias en runtimeNingunaUna
RoutersNext.js, React Router v6/v7, Remix, helpers para JS puroNext.js, React Router, Remix, TanStack Router, React puro

Tamaños: import de toda la librería, esbuild minify + gzip, medido en agosto de 2026 contra nuqs 2.10.1.

nuqs es una buena librería: elígela si quieres cada valor como su propio query param legible o usas TanStack Router. Elige state-in-url cuando quieras un objeto tipado completo en la URL sin configuración.

Lee la comparación completa — la misma feature en ambas y cómo migrar

¿Por qué state-in-url?

Existen bibliotecas de estado en la URL, pero la mayoría son engorrosas de configurar o limitadas en lo que pueden almacenar. state-in-url aspira a ser la que simplemente funciona: una API que imita React.useState, con la URL como almacén.

Guarda estado sin código repetitivo, construye enlaces profundos y comparte datos entre componentes de cliente no relacionados, sin necesidad de provider. La estructura y los tipos se conservan de extremo a extremo: un Date entra, un Date sale.

Construido con test-first, con suites unitarias y e2e entre navegadores ejecutándose en cada commit.

Next.js: sin límite de Suspense

El hook nunca llama a useSearchParams, por lo que un componente que lo usa no necesita envolverse en Suspense y no excluye su página del prerenderizado: PPR y cacheComponents incluidos. Lee la URL directamente y sigue cada cambio posterior, incluido un history.pushState desde código que no sabe nada de él.

¿No usas Next.js o react-router?

Los helpers encodeState / decodeState funcionan con cualquier framework o JS puro: los hooks son una comodidad encima.

Échale un vistazo a la página de GitHub : una estrella ayuda mucho.

Compártelo con otros desarrolladores

Uneed Embed Badge