Tema
Leer y modificar query params
Los parámetros de la URL (?categoria=cafe&orden=precio&pagina=2) permiten que los filtros de un catálogo se puedan compartir por WhatsApp, guardar en favoritos y sobrevivir a una recarga. También sirven para leer campañas (utm_source), códigos de referido o prellenar formularios. Con URLSearchParams es muy sencillo.
Lo básico
js
// URL actual: https://tienda.mx/catalogo?categoria=cafe&orden=precio&etiqueta=oferta&etiqueta=nuevo
const params = new URLSearchParams(window.location.search);
params.get('categoria'); // "cafe"
params.get('pagina'); // null (no existe)
params.has('orden'); // true
params.getAll('etiqueta'); // ["oferta", "nuevo"]
Object.fromEntries(params); // { categoria: "cafe", orden: "precio", etiqueta: "nuevo" } (el último gana)
// Crear una query string a partir de un objeto (codifica acentos y espacios)
new URLSearchParams({ q: 'café orgánico', pagina: 2 }).toString(); // "q=caf%C3%A9+org%C3%A1nico&pagina=2"Helpers reutilizables
js
/** Lee todos los parámetros como objeto, con tipos y valores por defecto */
export function leerParams(defectos = {}) {
const params = new URLSearchParams(location.search);
const resultado = { ...defectos };
for (const [clave, defecto] of Object.entries(defectos)) {
if (!params.has(clave)) continue;
const valor = params.get(clave);
// Convierte al tipo del valor por defecto
if (typeof defecto === 'number') resultado[clave] = Number(valor) || defecto;
else if (typeof defecto === 'boolean') resultado[clave] = valor === '1' || valor === 'true';
else if (Array.isArray(defecto)) resultado[clave] = params.getAll(clave);
else resultado[clave] = valor;
}
return resultado;
}
/**
* Actualiza la URL sin recargar la página.
* Valores null, undefined o '' eliminan el parámetro.
* @param {object} cambios
* @param {{ reemplazar?: boolean }} opciones reemplazar=true no crea entrada en el historial
*/
export function actualizarParams(cambios, { reemplazar = false } = {}) {
const url = new URL(location.href);
for (const [clave, valor] of Object.entries(cambios)) {
url.searchParams.delete(clave);
if (Array.isArray(valor)) valor.forEach((v) => url.searchParams.append(clave, v));
else if (valor !== null && valor !== undefined && valor !== '') url.searchParams.set(clave, valor);
}
history[reemplazar ? 'replaceState' : 'pushState']({}, '', url);
}
/** Quita parámetros de la URL visible (p. ej. tokens después de usarlos) */
export function limpiarParams(...claves) {
const url = new URL(location.href);
claves.forEach((c) => url.searchParams.delete(c));
history.replaceState({}, '', url);
}Caso real: catálogo con filtros en la URL
html
<form id="filtros">
<input type="search" name="q" placeholder="Buscar...">
<select name="categoria">
<option value="">Todas</option>
<option value="cafe">Café</option>
<option value="accesorios">Accesorios</option>
</select>
<select name="orden">
<option value="recientes">Más recientes</option>
<option value="precio">Menor precio</option>
</select>
<label><input type="checkbox" name="oferta" value="1"> Sólo ofertas</label>
</form>
<div id="productos"></div>js
import { leerParams, actualizarParams } from './url.js';
const form = document.getElementById('filtros');
const DEFECTOS = { q: '', categoria: '', orden: 'recientes', oferta: false, pagina: 1 };
// 1) Al cargar: la URL manda → rellena el formulario
function sincronizarFormulario() {
const f = leerParams(DEFECTOS);
form.q.value = f.q;
form.categoria.value = f.categoria;
form.orden.value = f.orden;
form.oferta.checked = f.oferta;
return f;
}
// 2) Al cambiar un filtro: actualiza la URL y vuelve a cargar productos
form.addEventListener('change', () => {
actualizarParams({
q: form.q.value.trim(),
categoria: form.categoria.value,
orden: form.orden.value === 'recientes' ? null : form.orden.value, // no ensucies la URL con defaults
oferta: form.oferta.checked ? 1 : null,
pagina: null, // al filtrar, vuelve a la página 1
});
cargarProductos(leerParams(DEFECTOS));
});
form.addEventListener('submit', (e) => e.preventDefault());
// 3) Botones atrás/adelante del navegador
window.addEventListener('popstate', () => cargarProductos(sincronizarFormulario()));
async function cargarProductos(filtros) {
const qs = new URLSearchParams(Object.entries(filtros).filter(([, v]) => v !== '' && v !== false));
const res = await fetch(`/api/productos?${qs}`);
const { data } = await res.json();
document.getElementById('productos').textContent = `${data.length} productos`;
}
cargarProductos(sincronizarFormulario());Otros usos frecuentes
js
const p = new URLSearchParams(location.search);
// Prellenar un formulario desde un enlace: /contacto?servicio=diseno-web
const servicio = p.get('servicio');
if (servicio) document.querySelector('[name="servicio"]').value = servicio;
// Guardar la campaña de marketing para enviarla con el formulario
['utm_source', 'utm_medium', 'utm_campaign'].forEach((k) => {
if (p.has(k)) sessionStorage.setItem(k, p.get(k));
});
// Abrir una pestaña o modal concreto: /servicios?tab=precios
const tab = p.get('tab');
if (tab) document.querySelector(`[data-tab="${CSS.escape(tab)}"]`)?.click();
// Código de referido de un solo uso: léelo, guárdalo y quítalo de la URL
import { limpiarParams } from './url.js';
if (p.has('ref')) {
localStorage.setItem('referido', p.get('ref'));
limpiarParams('ref');
}
// Hash (#seccion) en lugar de query
const seccion = location.hash.slice(1); // "precios"Nunca insertes parámetros con innerHTML
elemento.innerHTML = params.get('nombre') permite inyectar código (?nombre=<img src=x onerror=alert(1)>). Usa siempre textContent o escapa el valor. Y no pongas datos sensibles (tokens, contraseñas) en la URL: quedan en el historial y en los logs del servidor.
pushState vs replaceState
Usa pushState cuando el cambio sea una "navegación" que el usuario quiera deshacer con el botón Atrás (cambiar de categoría). Usa replaceState para cambios menores que no deberían llenar el historial (cada tecla del buscador).