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. Úsalo en cualquier componente
ComponentA
'use client';

import { useUrlState } from 'state-in-url/next';
import { form } from './form';

export const ComponentA = () => {
  // see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState
  // useHistory: false to update sp on server component
  const { urlState, setState, setUrl } = useUrlState(form, { useHistory: true }); 

  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 { useUrlState } from 'state-in-url/next';
import { form } from './form';

// "searchParams" used to pass params from Server Components
export const ComponentB = ({ searchParams }: { searchParams?: object }) => {
  const { urlState } = useUrlState(form, { searchParams });

// will be defaultValue from `form` if not in url, no need to check

  return <div>name: {urlState.name}</div>
};
3. Crea un hook reutilizable para una parte del estado
useFormState - custom hook
'use client';

import React from 'react';
import { useUrlState } from 'state-in-url/next';

const form: Form={
  name: '',
  age: undefined,
  agree_to_terms: false,
  tags: [],
};

type Form = {
  name: string;
  age?: number;
  agree_to_terms: boolean;
  tags: {id: string; value: {text: string; time: Date } }[];
};

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.

¿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