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 next.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
'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 });
3. Úsalo en cualquier componente — todos lo comparten
ComponentA
'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>
    </>
};
ComponentB
'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>
};
4. Haz crecer el hook cuando necesites más
useFormState - extended
'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 install

Ejecuta 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 funcionalidad
  • input-handlingCampos de texto, deslizadores, cualquier cosa que cambie rápido
  • nextjs-ssrApp Router: reenvío de searchParams, Proxy para layouts
  • react-router-remix-setupConfiguración de React Router v6/v7 o Remix v2
  • form-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-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