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

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.

La misma feature, en ambas

Un panel de filtros: un texto de búsqueda, un número de página, una lista de tags y una fecha. nuqs declara un parser por clave y conecta un adaptador en la raíz; state-in-url toma el objeto y lo envuelve en un hook reutilizable.

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[]
};

Ese único hook personalizado es toda la API de la feature: cada componente que lo llama comparte el mismo estado tipado — la lista de tags sigue siendo un array y la fecha vuelve como un objeto Date real. Sin props, sin context, sin cableado por clave.

Configuración y boilerplate

nuqs se conecta a tu router mediante un componente adaptador que envuelve la app, y cada pieza de estado declara su parser. state-in-url trae un hook por router: importa el que corresponda, pásale un objeto de estado por defecto y listo. Nada envuelve nada.

Next.js, SSR y prerenderizado

En el App Router, state-in-url nunca llama a useSearchParams, así que los componentes que lo usan no necesitan un límite de Suspense y sus páginas siguen prerenderizándose — PPR incluido. Los componentes de servidor leen el mismo estado por la prop searchParams, reenviada tal cual.

Migrar desde nuqs

Casi siempre es mecánico: reúne las claves de una feature en un único objeto de estado por defecto, elimina las declaraciones de parsers — los valores tipados llevan la misma información — y sustituye los setters por clave por un único setter que acepta un partial. Cada campo de primer nivel sigue siendo su propio parámetro de query.

Cómo se comparan las demás opciones

nuqs no es la única alternativa. El mismo trabajo — estado tipado en la query string — también lo cubren los routers y librerías más antiguas, cada una con su compromiso.

LibreríaConfiguraciónObjetos anidados y fechasTamañoElígela cuando
state-in-urlNinguna — importa el hookConservados automáticamente, tipos incluidos~2,9 KB gzip, cero depsQuieres un objeto tipado sin configuración en Next.js, React Router o Remix
nuqsComponente adaptador, parser por claveParser JSON más tu propio validador~6,7 KB gzip, una depQuieres cada valor como su propio query param legible
TanStack RoutervalidateSearch en cada rutaJSON-first para objetos y arrays; fechas con serialización propiaIntegrado en el routerUsas TanStack Router — usa lo que trae
use-query-paramsProvider más adaptador de router, config por parámetroMediante un tipo de parámetro JSON, tipado laxo~4,4 KB gzip más serialize-query-paramsUna base de código ya construida sobre ella
useSearchParamsNinguna — integrado en el routerSolo strings — parseo, tipos y defaults corren de tu cuenta0 KBUno o dos parámetros planos de texto, sin librería

Preguntas frecuentes

¿Es state-in-url una buena alternativa a nuqs?
Sí, cuando quieres un objeto tipado completo en la URL sin configuración: sin componente adaptador, sin parsers por clave, y con objetos anidados y fechas conservados automáticamente. nuqs sigue siendo mejor opción si quieres cada valor como su propio query param legible o usas TanStack Router.
¿Cuál es más pequeña, state-in-url o nuqs?
Medido con esbuild (minify + gzip, import de toda la librería) en agosto de 2026: state-in-url ronda los 2,9 KB con cero dependencias; nuqs 2.10.1 ronda los 6,7 KB con una dependencia. Importar un subconjunto reduce ambas.
¿Necesita state-in-url un adaptador o provider?
No. Cada router tiene su propio entry point: importa el hook correspondiente, pásale un objeto de estado por defecto y funciona. No hay componente adaptador que envuelva la app ni provider de contexto que configurar.
¿Es difícil migrar de nuqs a state-in-url?
Suele ser mecánico: reúne las claves de una feature en un único objeto de estado por defecto, elimina las declaraciones de parsers y sustituye los setters por clave por un único setter con un partial. Cada campo de primer nivel sigue siendo su propio parámetro de query.
¿Y los search params de TanStack Router?
Si usas TanStack Router, usa lo que trae: search params JSON-first validados por ruta con validateSearch. state-in-url y nuqs importan cuando tu router es Next.js, React Router o Remix, donde no hay search params tipados integrados.