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-urluseUrlState — en vivo con next.js
Escribe abajo — observa cómo se enciende la URL
Lee desde la URL — sin props, sin context, los tipos y la estructura se conservan
{name: stringage: undefinedundefinedagree_to_terms: falsebooleantags: []}
La misma API, tres routers
Inicio rápido
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 } }[];
};'use client';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
// One hook per feature - the whole API for this state
// "searchParams" only needed to pass params from Server Components
export const useFormState = (searchParams?: object) =>
useUrlState(form, { searchParams });'use client';
import { useFormState } from './useFormState';
export const ComponentA = () => {
// see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState
const { urlState, setState, setUrl } = 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>
</>
};'use client';
import { useFormState } from './useFormState';
// "searchParams" used to pass params from Server Components
export const ComponentB = ({ searchParams }: { searchParams?: object }) => {
// same state as ComponentA - no props, 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>
};'use client';
import React from 'react';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
export const useFormState = ({ searchParams }: { searchParams?: object }) => {
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 };
};¿Usas un agente de codificación con IA?
Los agentes se equivocan siempre en las mismas dos cosas aquí. Escriben la forma del estado con interface, que la restricción JSONCompatible rechaza de plano. Y construyen el objeto de estado por defecto dentro del componente, lo que rompe el uso compartido en silencio: se indexa por identidad del objeto, así que nada da error, los dos componentes simplemente dejan de verse.
Así que el paquete incluye seis SKILL.md archivos. Tu agente carga uno bajo demanda a través de TanStack Intent, y se versionan con la biblioteca y no con esta página.
npx @tanstack/intent@latest installEjecuta una vez en un proyecto que ya tenga state-in-url instalado. Tu agente encontrará entonces las habilidades en node_modules/state-in-url/skills/.
feature-state-hookDefinir el estado y envolver useUrlState en un hook de ámbito de funcionalidadinput-handlingCampos de texto, deslizadores, cualquier cosa que cambie rápidonextjs-ssrApp Router: reenvío de searchParams, Proxy para layoutsreact-router-remix-setupConfiguración de React Router v6/v7 o Remix v2form-library-integrationCombinación con react-hook-form (o formik)shared-state-no-urluseSharedState — compartir sin tocar la URL
Las fuentes están en GitHub. Un agente que no pueda cargar las habilidades de Intent debería leer llms.txt en su lugar — las mismas reglas, condensadas en un archivo.
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-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.
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

