wandres.dev
TAXONOMÍA DEL ESTADO · local, global, servidor, URL

Estado de la URL: la fuente de verdad navegable

Lo que debe ser compartible, navegable y recargable vive en la URL, no en un useState: filtros, página, pestaña, orden, búsqueda. La URL como store global serializable y direccionable, y las herramientas tipadas de 2026 (URLSearchParams, nuqs, routers).

⏱ 15 min

Existe una clase de estado que casi nadie coloca donde debe: el estado navegable. Los filtros de una tienda, el número de página, la pestaña activa, el término de búsqueda, el orden de una tabla. Instintivamente todo eso acaba en un useState, y con ello se pierde algo valiosísimo: la capacidad de compartir la vista por un enlace, de volver atrás con el navegador, de recargar sin perder el contexto. La URL no es solo una dirección: es el único estado que el usuario puede copiar, guardar en favoritos y enviar a otra persona. Es un store, y uno muy especial.

🎯 Al terminar esta lección sabrás
  • Entender la URL como un store global, serializable y direccionable.
  • Reconocer qué estado debe vivir en la URL y cuál no.
  • Sincronizar la vista con los search params sin duplicar la fuente de la verdad.
  • Usar las herramientas tipadas de 2026: URLSearchParams, nuqs y routers con search validados.

La URL es un store global serializable

Mira una URL con ojos de arquitecto de estado: tienda/camisetas?color=azul&talla=m&page=2&sort=precio. Cada parámetro es una entrada de un store. Pero es un store con cuatro superpoderes que ningún useState ni Zustand te dan gratis: es compartible (el enlace reproduce la vista exacta en otro navegador), navegable (el botón atrás recorre el historial de estados), recargable (F5 no destruye nada) y direccionable (un deep link lleva a un estado concreto). Esas cuatro propiedades no son un extra: son el contrato de la web. Ninguna librería de estado de cliente te las regala: nacen de que la dirección es, literalmente, texto que el navegador entiende, sincroniza entre pestañas y guarda en el historial. Cuando pones ahí el estado navegable, heredas todo ese comportamiento sin escribir una línea.

const params = new URLSearchParams(location.search);
const pagina = Number(params.get('page') ?? '1');
const filtro = params.get('q') ?? '';

// al cambiar el filtro, escribes en la URL — no en un useState
params.set('q', nuevoFiltro);
params.set('page', '1'); // resetea la pagina al filtrar
history.pushState(null, '', `?${params.toString()}`);

Conviene ver las tres regiones donde puede vivir el estado dentro de una URL, cada una con su semántica. Los path params (/producto/42) identifican el recurso principal y forman parte de la ruta. Los search params (?color=azul&page=2) modelan el estado secundario y opcional de la vista: filtros, orden, paginación. Y el hash (#seccion-3) apunta a una posición dentro de la página. La gran mayoría del estado navegable vive en los search params, porque son opcionales, combinables y no rompen la ruta si faltan.

💡
pushState o replaceState: la higiene del historial

Al escribir en la URL eliges si añades una entrada nueva al historial (pushState) o reemplazas la actual (replaceState). La regla: pushState para cambios que el usuario esperaría deshacer con el botón atrás (cambiar de pestaña, aplicar un filtro relevante) y replaceState para actualizaciones continuas que no merecen una entrada cada una (teclear en un buscador, arrastrar un deslizador). Ignorar esta distinción produce el bug clásico del botón atrás que hay que pulsar veinte veces para escapar de una sola página.

🔗

Compartible

Copiar y pegar el enlace reproduce exactamente la misma vista para otra persona. El estado viaja en el texto.

⏮️

Navegable

El botón atrás del navegador recorre el historial de estados. Cada cambio es un punto al que volver.

🔄

Recargable

Recargar la página no pierde el contexto: los filtros y la página siguen ahí porque viven en la URL.

📌

Direccionable

Un deep link apunta a un estado concreto de la app. La URL es una dirección de estado, no solo de página.

Qué debe vivir en la URL (y qué no)

La regla es directa: si el usuario querría reproducir, compartir o recuperar esta vista, su estado va en la URL. Filtros, búsqueda, paginación, orden, pestaña seleccionada, rango de fechas de un informe — todo eso es navegable por naturaleza. Lo que no va en la URL es lo efímero y privado: si un menú está abierto, el texto a medio escribir de un campo, el hover de un botón. Eso es estado local (lección 1). El límite fino aparece con los casos intermedios: el término de búsqueda mientras se teclea suele quedarse local (para no ensuciar el historial con cada tecla) y solo se promueve a la URL al confirmar o tras un debounce. Decidir dónde cae cada dato en ese gradiente es, precisamente, el oficio de esta taxonomía.

ℹ️
La URL como sincronizador entre pestañas y sesiones

Un beneficio que se subestima: al vivir en la URL, el estado navegable es implícitamente persistente y transversal. Dos pestañas con la misma URL muestran la misma vista sin ninguna coordinación por tu parte. Un usuario que guarda un enlace en favoritos vuelve semanas después al estado exacto. No escribiste una línea de persistencia ni de sincronización: la plataforma te la regala porque el estado vive en el sitio correcto.

Este es, además, el mecanismo del deep linking como característica de producto: un panel de control filtrado, un informe con un rango de fechas concreto o una vista de mapa centrada en un punto se convierten en enlaces que un equipo comparte en un chat o incrusta en un documento. Lo que para el usuario es “mándame ese enlace” es, por dentro, estado serializado en la URL. Ninguna otra clase de estado habilita esa colaboración sin construir infraestructura a medida.

flowchart LR
U[URL con search params] --> P[parsear params]
P --> V[render de la vista]
V --> I[el usuario filtra pagina u ordena]
I --> W[escribir en la URL]
W --> U
style U fill:#89b4fa,color:#11111b
style V fill:#a6e3a1,color:#11111b
style W fill:#cba6f7,color:#11111b

El ciclo es una sola dirección de la verdad: la URL manda, la vista la refleja, la interacción reescribe la URL. En cuanto duplicas ese estado en un useState “para que sea más cómodo”, creas dos fuentes de la verdad que se desincronizan — el clásico bug del filtro que dice una cosa y la lista muestra otra.

Herramientas tipadas de 2026

Trabajar con URLSearchParams a mano funciona, pero todo son strings y hay que serializar y parsear en cada punto. El ecosistema de 2026 resolvió esto con capas tipadas. nuqs es la referencia en React: expone la URL con una API idéntica a useState, pero la fuente de la verdad es la barra de direcciones.

import { useQueryState, parseAsInteger } from 'nuqs';

function Listado() {
  const [q, setQ] = useQueryState('q', { defaultValue: '' });
  const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
  // q y page se leen y escriben como useState, pero VIVEN en la URL:
  // compartibles, navegables y recargables sin esfuerzo extra
}

En la misma línea, TanStack Router valida los search params con un esquema (Zod, Valibot) y te los entrega tipados, y los routers de Next.js y Astro exponen la URL como primera clase. La sintaxis converge en una idea: darte la ergonomía de useState sin renunciar a que el estado esté donde el usuario puede alcanzarlo. Un detalle que estas capas aciertan: al cambiar un filtro suelen resetear la página a 1 y hacer routing superficial (sin recargar datos que no cambiaron), dos cosas fáciles de olvidar a mano.

📝
La URL es el único estado que el servidor ya conoce

En SSR el servidor no ve tu useState ni tu store: se ejecuta antes de que el navegador hidrate nada. Pero sí ve la URL, porque llega en la petición. Por eso el estado navegable es el único que permite un primer render correcto en el servidor: si los filtros viven en la URL, la página se sirve ya filtrada, sin parpadeo ni salto de contenido. Guardar ese estado en memoria de cliente obliga a un segundo render tras hidratar. La URL, otra vez, no es solo comodidad para el usuario: es la única fuente de la verdad que cruza la frontera servidor-cliente.

Y hay un dividendo final, silencioso: como el estado navegable queda escrito en la URL, tu analítica lo captura gratis. Cada filtro aplicado, cada búsqueda, cada pestaña es una URL registrable, sin instrumentar eventos a mano. El estado, puesto en el sitio correcto, se vuelve además observable.

El coste: serialización y qué no poner en la URL

La URL no es gratis. Es un canal de texto plano, público y acotado, y eso impone tres disciplinas. Primero, todo se serializa a string: un número vuelve como "2", un booleano como "true", y hay que parsear en cada lectura (por eso existen las capas tipadas). Segundo, hay un límite práctico de longitud —del orden de 2000 caracteres—, así que no caben estructuras grandes. Tercero, y crítico: la URL es visible y se comparte, de modo que jamás debe llevar secretos ni datos personales.

// estado navegable complejo: serializa a un formato compacto, con cuidado
const filtros = { color: 'azul', tallas: ['m', 'l'] };
params.set('f', encodeURIComponent(JSON.stringify(filtros)));
// al leer: JSON.parse(decodeURIComponent(params.get('f') ?? '{}'))
// si necesitas esto a menudo, es senal de que quiza el estado no es navegable

Hay más asperezas al operar con URLSearchParams a pelo: una misma clave puede repetirse (?tag=a&tag=b) y recuperarla exige getAll en vez de get; el orden de los parámetros no está garantizado, lo que complica comparar dos URLs; y todo pasa por codificación de porcentaje. Son justo las fricciones que capas como nuqs esconden, pero conviene saber que están ahí debajo.

⚠️
La URL es pública: nunca metas secretos

Todo lo que pongas en la URL viaja en texto, queda en el historial del navegador, en los logs del servidor, en la cabecera referer de peticiones salientes y en las herramientas de analítica. Un token, un email, un identificador interno o cualquier dato personal en un search param es una fuga esperando a ocurrir. La regla es tajante: la URL es para lo que el usuario querría compartir, y nada que no querrías ver impreso en una valla publicitaria.

Si se comparte, se marca o se recarga: es de la URL

Hay una prueba de una sola frase que reordena la mitad de las decisiones de estado de una app: si un usuario pudiera querer, alguna vez, compartir por enlace, guardar en favoritos o recargar y volver a esta misma vista, entonces su estado pertenece a la URL — y ningún useState ni store global es aceptable como fuente de la verdad. La razón es que la URL es el único estado de la aplicación que forma parte del contrato de la plataforma: el botón atrás, el historial, los favoritos y el copiar-pegar son expectativas que el usuario trae de fábrica, y romperlas guardando los filtros en memoria es una forma silenciosa de hostilidad. Pero hay algo más profundo. La URL impone una disciplina saludable: obliga a que ese estado sea serializable a texto, plano y pequeño. No puedes meter un objeto gigante ni una función en la barra de direcciones, y esa restricción te empuja a modelar el estado navegable como lo que debe ser: un puñado de claves primitivas que describen una vista. Cuando aceptas la URL como fuente de la verdad para lo navegable, ganas gratis persistencia entre sesiones, sincronización entre pestañas, deep linking y analítica legible — todo por poner el dato en el sitio correcto en vez de en el cómodo. El estado de la URL es la prueba más clara de la tesis del track: la clase de estado dicta la herramienta, y elegir mal no es un detalle estético, es regalar o negar capacidades enteras al usuario.

⚔️ Sube el estado navegable a la URL
  1. Toma una vista con filtros o paginación en useState y anota cuáles debería poder compartir el usuario por enlace.
  2. Migra esos valores a la URL con URLSearchParams o nuqs, dejando la URL como única fuente de la verdad.
  3. Comprueba las cuatro propiedades: comparte el enlace, pulsa atrás, recarga y abre en dos pestañas.
  4. Asegúrate de resetear la página a 1 al cambiar un filtro, y de no duplicar el estado en un useState paralelo.
  5. Revisa que ningún dato sensible acabe en la URL y decide, en cada escritura, entre pushState y replaceState.