Herramientas: nuqs, el router como store y sus límites
Trabajar URLSearchParams a mano funciona, pero el ecosistema de 2026 resolvió la ergonomía con capas tipadas. Esta lección presenta nuqs, que da a la URL una API idéntica a useState con parsers tipados y valores por defecto, reencuadra el router como el store global del estado navegable con la validación por esquema de TanStack Router, y cierra el nivel con la disciplina inversa: los límites duros de la URL —longitud, secretos, estructuras grandes, alta frecuencia— que marcan cuándo un dato NO debe vivir ahí por mucho que sea tentador.
Manejar URLSearchParams a mano funciona y enseña la mecánica real, pero en producción todo son strings, todo hay que parsear en cada punto y el codec de la lección 2 acaba escribiéndose una y otra vez. El ecosistema de 2026 resolvió esa fricción con capas tipadas que esconden la serialización sin esconder el modelo: nuqs expone la URL con una API idéntica a useState, y los routers modernos validan los search params con un esquema y te los entregan tipados. La idea que las une es un reencuadre potente: el router deja de ser un enrutador de páginas y pasa a ser el store global de todo el estado navegable. Pero una herramienta que hace algo fácil también facilita hacerlo donde no toca, así que esta lección cierra el nivel con la disciplina inversa: los límites que dicen cuándo un dato, por tentador que sea, no debe vivir en la URL.
- Usar
nuqspara leer y escribir la URL con la ergonomía deuseStatey parsers tipados. - Reencuadrar el router como el store global del estado navegable, con validación por esquema.
- Aplicar la validación de search params de TanStack Router con Zod o Valibot.
- Reconocer los límites duros de la URL y decidir con criterio cuándo un dato no pertenece ahí.
nuqs: la URL con ergonomía de useState
nuqs es la referencia en React para el estado de la URL. Su acierto de diseño es ergonómico: expone useQueryState, un hook con la misma forma que useState —un valor y un setter—, pero la fuente de la verdad no es la memoria del componente, sino la barra de direcciones. Escribes el estado navegable como si fuera local y obtienes gratis las cuatro propiedades del contrato. Debajo, nuqs incorpora la disciplina de las lecciones anteriores: parsers tipados que convierten string a número, booleano, fecha o enum; valores por defecto que dan sentido a la ausencia y canonizan la escritura; y una elección explícita entre empujar o reemplazar historial.
import { useQueryState, parseAsInteger, parseAsStringEnum } from 'nuqs';
function Listado() {
// q se lee y escribe como useState, pero VIVE en la URL
const [q, setQ] = useQueryState('q', { defaultValue: '' });
// page llega ya tipado como number, con defecto 1
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
// el enum queda acotado a valores validos; la basura colapsa al defecto
const [sort, setSort] = useQueryState(
'sort',
parseAsStringEnum(['precio', 'novedad']).withDefault('novedad'),
);
// alta frecuencia: reemplaza historial en vez de empujar una entrada por tecla
const onType = (v: string) => setQ(v, { history: 'replace' });
}
Ese bloque condensa todo el nivel: parseo total por los parsers, canonización por los defectos, e higiene de historial por la opción history. Lo que en crudo eran veinte líneas de codec disperso, aquí es declarativo y tipado. nuqs incluso resetea dependencias entre parámetros con transiciones agrupadas —cambiar el filtro y volver a página 1 en una sola escritura— que a mano es fácil olvidar.
El mayor valor de nuqs no es ahorrar tecleo, sino que hace que lo correcto sea también lo cómodo. Cuando poner un filtro en la URL cuesta lo mismo que un useState, desaparece el incentivo perverso que empujaba el estado navegable a la memoria “por comodidad”. La herramienta alinea el camino de menor resistencia con la arquitectura correcta, y esa alineación cambia el comportamiento por defecto de un equipo entero mucho más que cualquier documento de buenas prácticas.
Bajo la superficie, nuqs resuelve además dos asperezas que a mano se olvidan. Hace routing superficial —escribe la URL sin desencadenar una recarga de datos que no cambiaron— y agrupa varias escrituras en una sola transición, de modo que cambiar el filtro y reiniciar la página no produce dos entradas de historial ni dos renders. Son exactamente las fricciones de las lecciones 2 y 3 resueltas de fábrica: parseo, canonización e higiene de historial dejan de ser código tuyo y pasan a ser configuración del hook.
El router como store del estado navegable
Sube un nivel de abstracción y aparece el reencuadre decisivo: el router es tu store global para el estado navegable. Durante años pensamos el router como un mapa de rutas a componentes; los routers de 2026 lo tratan como lo que también es —el dueño de la URL— y por tanto la fuente reactiva del estado que vive en ella. TanStack Router lleva esto al extremo tipado: declara un esquema de search params por ruta, valida la URL entrante contra él y entrega a los componentes un objeto ya parseado y con tipos, con el codec de la lección 2 convertido en configuración.
import { createFileRoute } from '@tanstack/react-router';
import { z } from 'zod';
const busqueda = z.object({
q: z.string().catch(''), // parseo total: catch da el defecto
page: z.number().int().min(1).catch(1),
sort: z.enum(['precio', 'novedad']).catch('novedad'),
});
export const Route = createFileRoute('/catalogo')({
validateSearch: busqueda, // el esquema ES el codec
component: () => {
const { q, page, sort } = Route.useSearch(); // tipado, validado, canonico
const navigate = Route.useNavigate();
// escribir estado navegable es navegar: la URL es la unica fuente
const filtrar = (nuevo: string) => navigate({ search: (s) => ({ ...s, q: nuevo, page: 1 }) });
},
});
El catch de Zod no es un detalle: es el parseo total de la lección 2 hecho declarativo, de modo que una URL manipulada nunca lanza, solo degrada al defecto. Y navigate con una función que transforma el search anterior es el flujo de una sola dirección de la lección 4 hecho API: no hay estado local que sincronizar, escribir es navegar, y la vista se deriva del search validado. Next.js y Astro exponen la misma idea con useSearchParams y con la URL de primera clase en el servidor; la sintaxis varía, la arquitectura converge. Reconocer que el router es el store del estado navegable colapsa una enorme cantidad de complejidad accidental: deja de haber una capa de estado paralela al router que hay que mantener en sincronía con él.
flowchart LR URL[URL] -->|validateSearch esquema| T[search tipado y canonico] T -->|useSearch| C[componentes derivan la vista] C -->|navigate| URL R[router como store del estado navegable] --- URL style URL fill:#89b4fa,color:#11111b style T fill:#a6e3a1,color:#11111b style R fill:#cba6f7,color:#11111b
El mismo patrón fuera de nuqs
nuqs es específico de React, pero la idea del router como store del estado navegable es transversal y conviene reconocerla en cada ecosistema para no confundir el modelo con una librería concreta. En Next.js, useSearchParams y el router del App Router exponen la URL como fuente reactiva, y en el servidor los search params llegan como props de la página sin cliente alguno. Astro entrega Astro.url en el servidor y delega el trabajo fino de cliente a la isla que elijas. React Router expone useSearchParams con una API deliberadamente parecida a useState. Y por debajo de todos ellos, las librerías de serialización tipada resuelven el mismo problema del codec de la lección 2: convertir el string público en el tipo privado.
La lección de fondo no es qué import escribir, sino que en 2026 ninguna de estas herramientas te pide tratar la URL como un caso especial: todas la modelan como estado reactivo de primera clase, y la sintaxis converge porque el modelo mental es único. Aprende el modelo —fuente canónica, parseo total, escritura como navegación— y la herramienta concreta se vuelve un detalle intercambiable que eliges por el framework que ya usas, no por una diferencia de fondo.
Cuándo NO poner algo en la URL
Una herramienta que vuelve trivial escribir en la URL también vuelve trivial abusar de ella, así que el criterio inverso es tan importante como el positivo. La URL es un canal de texto plano, público y acotado, y esas tres palabras dibujan sus límites duros. Público: jamás debe llevar secretos ni datos personales, porque viaja en el historial, en los logs del servidor, en la cabecera referer de peticiones salientes y en la analítica; un token, un email o un identificador interno en un search param es una fuga esperando a ocurrir. Acotado: hay un límite práctico de longitud, del orden de dos mil caracteres, de modo que no caben estructuras grandes ni listas largas. Y texto plano: cada dato se serializa y hay que parsearlo, lo que penaliza el estado complejo o de muy alta frecuencia si cada cambio reescribe la URL sin debounce.
Secretos y datos personales
Nunca. La URL es pública y persistente en logs e historial. Tokens, emails, identificadores internos o cualquier PII quedan expuestos y registrados fuera de tu control.
Estructuras grandes
El límite práctico ronda los 2000 caracteres. Un objeto anidado o una lista larga no caben; si necesitas serializar JSON a un param, suele ser señal de que ese estado no era navegable.
Estado efímero
Un menú abierto, un hover, el foco, un borrador sin confirmar: nadie querría compartir ni recuperar eso. Es estado local; meterlo en la URL solo la ensucia.
Alta frecuencia sin debounce
Un deslizador o un mapa que se mueve escribirían decenas de entradas por segundo. Requiere buffer local y promoción con debounce y replace, o no pertenece a la URL.
El caso que más engaña es el de la estructura grande, porque las herramientas lo permiten y hasta ofrecen serializar JSON a un parámetro. Cuando te descubres codificando un objeto anidado en un search param, la pregunta correcta no es cómo comprimirlo, sino si ese estado era navegable de verdad. Casi siempre la respuesta es que no: era estado de aplicación que se coló en la URL por inercia. La disciplina del nivel se cierra sobre sí misma —la restricción de la URL a texto plano y pequeño no es un obstáculo a rodear, sino una prueba que confirma o niega que el dato pertenecía ahí—.
La síntesis del nivel tiene dos caras que hay que sostener a la vez. La primera es un reencuadre: el router no es un enrutador de páginas con un módulo de query params de propina, es el store global de todo el estado navegable de tu aplicación, y herramientas como nuqs y la validación por esquema de TanStack Router existen para que trabajar ese store sea tan cómodo y tan tipado como cualquier estado local. Cuando interiorizas ese reencuadre, una capa entera de complejidad accidental desaparece: ya no hay un estado paralelo al router que mantener en sincronía con la URL, porque el router es la URL tipada, y escribir estado navegable es simplemente navegar. La segunda cara es la disciplina inversa, y es la que separa al que usa la herramienta del que la domina: precisamente porque nuqs hace trivial poner cualquier cosa en la URL, el criterio de qué no poner se vuelve más importante, no menos. Los tres límites —público, acotado, textual— no son inconvenientes a sortear con trucos de compresión o cifrado casero; son la frontera de aplicabilidad de toda la clase de estado. Un secreto en la URL no es un bug de configuración, es una violación del contrato de que la URL es compartible: lo que es compartible por diseño no puede a la vez ser secreto. Una estructura de mil caracteres en un param no es un problema de longitud, es la señal de que ese dato nunca fue navegable y se coló por inercia. Y la restricción a texto plano y pequeño, lejos de ser una pobreza, es el filtro que te obliga a modelar el estado navegable como lo que debe ser: un puñado de claves primitivas que describen una vista. La madurez completa del tema es sostener las dos caras sin soltar ninguna: usar el router como el store que es, con toda la ergonomía tipada de 2026, y respetar sus límites como parte de su corrección, sabiendo que la clase de un dato de estado —no la comodidad de la herramienta— es lo que decide dónde vive.
- Migra el codec manual de una vista a
nuqso avalidateSearchcon Zod, reproduciendo parseo total, defectos canónicos e higiene de historial. - Comprueba que la basura degrada al defecto: teclea
?page=abc&sort=inventadoy verifica que la vista no rompe. - Reencuadra tu router como store: localiza cualquier estado navegable que hoy viva fuera de él y devuélvelo a la URL tipada.
- Audita cada search param contra los cuatro límites: ¿hay secretos o datos personales, estructuras grandes, estado efímero o alta frecuencia sin
debounce? - Para cada violación, decide el destino correcto —
localStorage, memoria local, servidor— y saca de la URL lo que nunca fue navegable.