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 / Astro
  • MIT
npm i state-in-url

useUrlState — en vivo con astro

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, cuatro frameworks

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/astro';
import { form } from './form';

// One hook per feature - the whole API for this state.
// searchParams is the island prop: with it the server render matches the URL
export const useFormState = (searchParams?: Record<string, string>) =>
  useUrlState(form, { searchParams });
3. Úsalo en cualquier componente — todos lo comparten
index.astro
---
import { ComponentA } from '../components/ComponentA';
import { ComponentB } from '../components/ComponentB';

// A plain object: island props are serialized, URLSearchParams is not
const searchParams = Object.fromEntries(Astro.url.searchParams);
---

<ComponentA client:load searchParams={searchParams} />
<ComponentB client:load searchParams={searchParams} />
ComponentA
import { useFormState } from './useFormState';

export const ComponentA = ({ searchParams }: { searchParams?: Record<string, string> }) => {
  // see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/astro/useUrlState
  const { urlState, setUrl, setState } = useFormState(searchParams);

  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 = ({ searchParams }: { searchParams?: Record<string, string> }) => {
  // same state as ComponentA - a separate island, no props between them, 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
import React from 'react';
import { useUrlState } from 'state-in-url/astro';
import { form } from './form';

export const useFormState = (searchParams?: Record<string, string>) => {
  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 };
};

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ónNext.js, React Router v6/v7, Remix, Astro, helpers para JS puroUn 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