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-urlstate-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-url | nuqs |
|---|---|---|
| Configuración | Ninguna — importa el hook y listo | Un componente adaptador envuelve la app |
| Forma del estado | Un objeto tipado, como React.useState | Valores por clave, con un parser declarado para cada una |
| Reutilización entre componentes | Envuelve el hook una vez — cada componente comparte el estado, sin props | Extraes tu propio hook alrededor del mapa de parsers |
| Objetos y arrays anidados | Integrado — estructura y tipos se conservan | Parser JSON más tu propio validador |
| Fechas | Se conservan automáticamente | Parser integrado, declarado por clave |
| Tamaño, import completo | ~2,9 KB gzip | ~6,7 KB gzip |
| Dependencias en runtime | Ninguna | Una |
| Routers | Next.js, React Router v6/v7, Remix, helpers para JS puro | Next.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
import { NuqsAdapter } from 'nuqs/adapters/next/app';
export default function RootLayout({ children }) {
return (
<html>
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
);
}'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 })}
/>
);
};'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ía | Configuración | Objetos anidados y fechas | Tamaño | Elígela cuando |
|---|---|---|---|---|
| state-in-url | Ninguna — importa el hook | Conservados automáticamente, tipos incluidos | ~2,9 KB gzip, cero deps | Quieres un objeto tipado sin configuración en Next.js, React Router o Remix |
| nuqs | Componente adaptador, parser por clave | Parser JSON más tu propio validador | ~6,7 KB gzip, una dep | Quieres cada valor como su propio query param legible |
| TanStack Router | validateSearch en cada ruta | JSON-first para objetos y arrays; fechas con serialización propia | Integrado en el router | Usas TanStack Router — usa lo que trae |
| use-query-params | Provider más adaptador de router, config por parámetro | Mediante un tipo de parámetro JSON, tipado laxo | ~4,4 KB gzip más serialize-query-params | Una base de código ya construida sobre ella |
| useSearchParams | Ninguna — integrado en el router | Solo strings — parseo, tipos y defaults corren de tu cuenta | 0 KB | Uno 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.
