La URL como fuente de verdad
Existe una clase de estado que casi todo el mundo coloca en el sitio equivocado: el estado navegable. Filtros, número de página, pestaña activa, término de búsqueda, orden de una tabla y rango de fechas de un informe pertenecen a la barra de direcciones, no a un useState. Esta lección define la URL como un store direccionable, serializable y público que forma parte del contrato de la plataforma, presenta el estado navegable como una clase de primera categoría, fija el criterio de una sola pregunta para decidir qué debe vivir ahí, y separa las tres regiones de una URL con sus semánticas propias.
Hay una clase de estado que casi nadie coloca donde debe. Los filtros de un catálogo, el número de página, la pestaña activa, el término de búsqueda, el orden de una tabla, el rango de fechas de un informe: por reflejo, todo eso acaba en un useState, y con ello se pierde algo valiosísimo. Se pierde la capacidad de compartir la vista por un enlace, de volver atrás con el navegador, de recargar sin perder el contexto, de guardar en favoritos un estado concreto. La URL no es una decoración de la página ni una mera dirección de recurso: es el único estado de tu aplicación que el usuario puede copiar, marcar y enviar a otra persona. Es un store, y uno con propiedades que ninguna librería de cliente te regala. Este nivel entero trata de reconocer esa clase de estado y devolverla a su sitio.
- Entender la URL como un store direccionable, serializable y público de la aplicación.
- Reconocer el estado navegable como una clase de primera categoría, definida por una propiedad.
- Aplicar el criterio de una sola pregunta para decidir qué estado debe vivir en la URL.
- Distinguir las tres regiones de una URL —path, search y hash— y su semántica.
La dirección es estado, no decoración
Mira una URL con ojos de arquitecto de estado y no de diseñador de rutas: catalogo/camisetas?color=azul&talla=m&page=2&sort=precio. Cada parámetro es una entrada de un store, y el conjunto describe por completo una vista. Pero es un store con cuatro propiedades que ningún useState, ningún Zustand y ningún contexto te dan gratis. Es compartible: el texto reproduce la vista exacta en otro navegador. Es navegable: el botón atrás recorre el historial de estados anteriores. Es recargable: pulsar F5 no destruye nada. Y es direccionable: un enlace apunta a un estado concreto, no solo a una página.
Lo decisivo es entender de dónde nacen esas propiedades. No son funcionalidades que una librería añada, sino consecuencias de una única decisión: el estado vive en texto que el navegador entiende, sincroniza entre pestañas, guarda en el historial y expone a copiar y pegar. Es memoria externa a tu aplicación, gobernada por la plataforma y no por tu framework. Por eso sobrevive a un recargado que destruye toda la memoria de JavaScript, y por eso un enlace funciona en una máquina que jamás ejecutó tu código. La URL es el único estado que forma parte del contrato de la web, y las cuatro propiedades no se construyen: se heredan por poner el dato en el sitio correcto.
const params = new URLSearchParams(location.search);
const page = Number(params.get('page') ?? '1');
const filtro = params.get('q') ?? '';
// al cambiar el filtro escribimos en la URL, no en un useState paralelo
params.set('q', nuevoFiltro);
params.set('page', '1'); // un filtro nuevo reinicia la paginacion
history.pushState(null, '', `?${params.toString()}`);
La consecuencia práctica es tajante. El mismo filtro guardado en memoria y guardado en la URL producen interfaces que parecen idénticas mientras nadie las toca, y radicalmente distintas en cuanto el usuario ejerce una de las cuatro afordancias. La memoria muere al recargar, no viaja en el enlace y no deja rastro en el historial; la URL hace las tres cosas sin una línea extra. Elegir entre ambas no es una preferencia de estilo: es decidir qué capacidades tendrá el usuario.
El estado navegable es una clase, no un caso
La taxonomía del estado que recorre este track distingue el estado local, el global de cliente, el del servidor y el de la URL. El error habitual es tratar los tres primeros como categorías serias y el cuarto como un rincón menor donde caen los filtros por casualidad. Es al revés: el estado navegable es una clase de primera categoría, y lo que la define no es un tipo de dato sino una propiedad —la de merecer ser direccionable—. Dos datos del mismo tipo, dos strings, pueden pertenecer a clases distintas: el término de una búsqueda confirmada es navegable, el texto de un campo a medio escribir no lo es, aunque ambos sean cadenas.
Reconocer esta clase cambia el orden de las decisiones. No preguntas primero “¿qué librería de estado uso?” y luego “¿la hago compartible?”; preguntas primero “¿este dato es navegable?”, y esa respuesta determina la ubicación antes de que la herramienta entre en escena. La clase precede a la herramienta, y en el caso de la URL la precede de forma especialmente nítida, porque la propiedad que define la clase —ser direccionable— solo la satisface un lugar.
Que dos datos compartan tipo no implica que compartan clase de estado. q = "botas" y borrador = "bota" son ambos strings, pero el primero describe una vista que alguien querría compartir y el segundo es un residuo efímero del teclado. Clasificar por tipo —“los strings van aquí, los números allá”— es el error de base; clasificar por propiedad —“¿esto merece ser compartido, marcado y recargado?”— es la disciplina. La misma idea reaparecerá en cada nivel del track: la clase de un dato no se lee en su tipo, se lee en lo que el usuario espera poder hacer con él.
El criterio de una sola pregunta
El criterio se vuelve operativo cuando lo reduces a una prueba que puedes recitar de memoria: si un usuario pudiera querer, alguna vez, compartir por enlace, guardar en favoritos o recargar y recuperar esta misma vista, entonces su estado pertenece a la URL. Filtros, búsqueda, paginación, orden, pestaña seleccionada, rango de fechas: todo eso es navegable por naturaleza, porque describe qué está mirando el usuario. 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, la posición del cursor, el hover de un botón. Ese estado es local, muere con la sesión y a nadie le importa reproducirlo.
El oficio aparece en el gradiente entre ambos extremos. El caso canónico es el buscador: el término final es navegable —quiero compartir la búsqueda de “botas de agua”—, pero cada pulsación intermedia no lo es, porque ensuciar el historial con veinte estados por una sola palabra es hostil. La resolución no es dudar, sino separar dos cosas que parecen una: el valor efímero mientras se teclea vive local, y solo se promueve a la URL al confirmar o tras un debounce.
function Buscador() {
const [borrador, setBorrador] = useState(''); // efimero: no navegable
const debounced = useDebounce(borrador, 300);
useEffect(() => {
// se PROMUEVE a la URL solo el valor estabilizado, ya navegable
const p = new URLSearchParams(location.search);
debounced ? p.set('q', debounced) : p.delete('q');
history.replaceState(null, '', `?${p}`); // replace: no ensucia el historial
}, [debounced]);
}
Conviene no confundir la URL con localStorage. Ambos sobreviven a un recargado, pero resuelven problemas distintos: localStorage recuerda una preferencia del dispositivo —el tema oscuro, el idioma— que no querrías incrustar en cada enlace. La URL recuerda una vista que sí querrías compartir. La preferencia es tuya y viaja contigo; la vista es del momento y viaja en el texto. Meter una preferencia global en la URL la vuelve ruidosa; meter una vista compartible en localStorage la vuelve incompartible. La pregunta que los separa es simple: ¿querría el usuario que esto viajara en el enlace que le pasa a un colega?
Las tres regiones y sus semánticas
Una URL no es un saco uniforme: tiene tres regiones con contratos distintos, y ubicar cada dato en la correcta es parte de la disciplina. Los path params —/producto/42— identifican el recurso principal y forman parte de la ruta; su ausencia cambia qué página es, no cómo se ve. Los search params —?color=azul&page=2— modelan el estado secundario y opcional de la vista: filtros, orden, paginación; su ausencia deja una vista por defecto perfectamente válida. Y el hash —#seccion-3— apunta a una posición dentro del documento y ni siquiera viaja al servidor.
Path: qué recurso
Identifica el recurso principal y es obligatorio para la ruta. Un identificador de producto o un slug de artículo van aquí, no en un search param.
Search: cómo se ve
Modela el estado navegable opcional y combinable: filtros, orden, página, búsqueda. Es la región donde vive la inmensa mayoría del estado de la URL.
Hash: dónde dentro
Marca una posición dentro de la página y no llega al servidor. Útil para anclas y navegación interna, rara vez para estado de datos.
La única que el servidor ve
En SSR el servidor no ve tu store ni tu useState, pero sí la URL, porque llega en la petición. Es el único estado que habilita un primer render correcto.
La gran mayoría del estado navegable vive en los search params, y no por casualidad: son opcionales, combinables y no rompen la ruta si faltan. Esa opcionalidad es exactamente la propiedad que necesita un filtro, que puede estar o no estar sin invalidar la página. Confundir las regiones —meter un filtro en el path, o el identificador del recurso en un search param— produce URLs frágiles: rutas que dejan de resolver cuando falta un dato que debería ser opcional, o recursos que se pierden al limpiar los parámetros.
Hay una restricción que la región de search params no perdona: todo lo que pongas ahí viaja en texto, queda en el historial del navegador, en los logs del servidor, en la cabecera referer de peticiones salientes y en la 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, que la última lección desarrollará, 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.
El ciclo completo, una vez el estado navegable está en la URL, tiene una sola dirección de la verdad, y verlo así previene el error que la lección 4 desmenuza: la URL manda, la vista la refleja, la interacción reescribe la URL, y no hay copia intermedia que pueda quedar obsoleta.
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
La tesis de todo el track alcanza aquí su forma más nítida: la clase de un dato de estado dicta la herramienta correcta, y elegir mal no es un descuido estético sino una decisión que regala o niega capacidades enteras al usuario. El estado navegable —lo que describe qué está mirando alguien— tiene una única ubicación correcta, la URL, porque solo ahí hereda las cuatro propiedades del contrato de la plataforma: compartir, navegar, recargar y direccionar. Guardar los filtros en un useState no es una alternativa más cómoda; es una amputación silenciosa. Rompe el botón atrás, invalida el favorito, borra la vista al recargar y hace incompartible lo que el usuario esperaba compartir, y lo hace sin un solo mensaje de error, porque la interfaz sigue pareciendo que funciona. Hay además un dividendo profundo y poco citado: la URL impone una disciplina saludable, porque 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 ganas gratis persistencia entre sesiones, sincronización entre pestañas, deep linking, primer render correcto en el servidor y analítica legible, todo por poner el dato en el sitio correcto en vez de en el cómodo. El ingeniero maduro no pregunta qué librería de estado usar para los filtros; pregunta si ese estado es navegable, y si la respuesta es sí, la ubicación ya está decidida antes de escribir una línea.
- Toma una vista con filtros, búsqueda o paginación y lista todo su estado, viva donde viva hoy.
- Aplica a cada dato la prueba de una sola pregunta: ¿querría el usuario compartirlo, marcarlo o recuperarlo al recargar?
- Clasifica cada pieza por su propiedad, no por su tipo: dos strings pueden caer en clases distintas, como el término confirmado frente al borrador.
- Para lo navegable, decide su región: ¿identifica el recurso —path— o describe la vista —search—?
- Comprueba qué se rompe hoy: comparte el enlace, pulsa atrás y recarga, y anota cada propiedad del contrato que tu implementación actual está negando.