Ingeniería5 min de lectura
Cómo gestionar el consentimiento de cookies en aplicaciones SPA
Consentimiento de cookies en React, Vue y otras SPA: estado compartido, seguimiento de rutas, hidratación SSR, contenidos bloqueados y retirada inmediata.
En una aplicación de una sola página (SPA), la página nunca se recarga por completo, así que el consentimiento de cookies tiene que vivir en el estado de la aplicación y no en un script que se ejecuta una sola vez. Guarda la elección del visitante, compártela con todos los componentes, activa o bloquea scripts cuando cambie, envía páginas vistas en los cambios de ruta solo con consentimiento y haz que la retirada surta efecto de inmediato, no en la siguiente carga de página.
¿Por qué es más difícil el consentimiento de cookies en una aplicación de una sola página?
La mayoría de los patrones de consentimiento dan por hecho que hay cargas de página completas, mientras que una SPA se carga una vez y después cambia de vista con JavaScript. Eso rompe cuatro supuestos:
- Los scripts se cargan una sola vez. Si la analítica se bloqueó al cargar y el visitante acepta más tarde, ninguna carga de página la recogerá. Tienes que activarla sobre la marcha.
- Las páginas vistas son virtuales. Los cambios de ruta no recargan la página, así que tu router debe notificar las páginas vistas, y antes debe comprobar el consentimiento.
- Las decisiones cambian a mitad de sesión. Un visitante puede aceptar, navegar y después retirar el consentimiento desde un enlace del pie de página mientras los scripts ya se están ejecutando en memoria.
- El renderizado en el servidor añade la hidratación. Con frameworks como Next.js o Nuxt, el HTML del servidor debe coincidir con el primer renderizado del cliente, pero puede que el servidor no conozca la elección del visitante.
¿Cómo debo modelar el estado del consentimiento en una SPA?
Trata el consentimiento como un pequeño store del que lee cada parte de la aplicación, con un método de suscripción para que los componentes y los scripts reaccionen a los cambios. Pon todas las categorías no esenciales en denegado por defecto, para que el código que se ejecuta antes de cargar la elección guardada no pueda filtrar datos nunca.
// consent-store.ts
export type Category = 'necessary' | 'preferences' | 'analytics' | 'marketing';
export type Consent = Record<Category, boolean>;
export const DENIED: Consent = {
necessary: true, preferences: false, analytics: false, marketing: false,
};
let state: Consent = DENIED;
const listeners = new Set<() => void>();
export const consentStore = {
get: (): Consent => state,
set(next: Partial<Consent>) {
state = { ...state, ...next, necessary: true };
listeners.forEach((notify) => notify());
},
subscribe(listener: () => void) {
listeners.add(listener);
return () => { listeners.delete(listener); };
},
};
En una aplicación real, set también guardaría la elección en una cookie propia, la anotaría en tu registro de consentimiento y actualizaría Google Consent Mode con gtag('consent', 'update', …). En React, useSyncExternalStore conecta el store con los componentes, y el mismo hook puede bloquear contenidos incrustados que instalan cookies:
// consent.tsx
import { useSyncExternalStore } from 'react';
import { consentStore, DENIED } from './consent-store';
export function useConsent() {
return useSyncExternalStore(consentStore.subscribe, consentStore.get, () => DENIED);
}
export function VideoEmbed({ videoId }: { videoId: string }) {
const { marketing } = useConsent();
if (!marketing) {
return (
<div className="embed-placeholder">
<p>Este vídeo está alojado en YouTube, que puede instalar cookies de marketing.</p>
<button type="button" onClick={() => consentStore.set({ marketing: true })}>
Permitir cookies de marketing y reproducir
</button>
</div>
);
}
return (
<iframe
src={`https://www.youtube-nocookie.com/embed/${videoId}`}
title="Vídeo del producto"
allowFullScreen
/>
);
}
El tercer argumento de useSyncExternalStore es la instantánea que se usa durante el renderizado en el servidor y la hidratación. Devolver DENIED ahí hace que el servidor y el primer renderizado del cliente coincidan, y React vuelve a renderizar con el estado real justo después de la hidratación. En Vue, un composable que envuelve un ref reactivo cumple la misma función.
Para los scripts que no son componentes, suscríbete directamente al store y cárgalos con import() dinámico en cuanto se conceda su categoría. Nuestra guía para bloquear scripts hasta obtener el consentimiento explica los patrones de activación.
¿Cómo hago seguimiento de los cambios de ruta solo con consentimiento?
Envía una página vista virtual desde el router después de cada navegación y comprueba el consentimiento en ese momento. Si el visitante concede el consentimiento para la analítica a mitad de sesión, es razonable registrar la página en la que está, porque es ahí donde empieza la aceptación.
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useConsent } from './consent';
import { trackPageView } from './analytics';
export function PageViewTracker() {
const { pathname } = useLocation();
const { analytics } = useConsent();
useEffect(() => {
if (analytics) trackPageView(pathname);
}, [pathname, analytics]);
return null;
}
Cuidado con el doble recuento. Algunas herramientas de analítica pueden enviar páginas vistas automáticamente cuando cambia el historial del navegador; Google Analytics 4 lo ofrece como opción de medición mejorada. Usa la opción automática o tu propio rastreador, pero no ambos.
¿Cómo evito las discrepancias de hidratación y el parpadeo del banner?
Decide dónde se conoce la elección del visitante en el primer renderizado. Hay dos opciones sólidas:
- Leer la cookie de consentimiento en el servidor. Las cookies se envían con cada solicitud, así que las páginas renderizadas en el servidor pueden mostrar u omitir el banner correctamente desde el primer byte. Esto solo funciona si el HTML no se almacena en caché ni se comparte entre visitantes.
- Renderizar el banner solo en el cliente. En páginas generadas de forma estática o almacenadas en la caché de una CDN, no renderices nada en el servidor y muestra el banner una vez montada la aplicación. Colócalo como una capa superpuesta fija para que una aparición tardía no provoque desplazamientos de diseño; consulta banners de cookies y Core Web Vitals.
En ambos casos, la cookie o la entrada de almacenamiento que recuerda la elección suele considerarse necesaria para respetar esa elección; consulta qué se considera una cookie estrictamente necesaria.
¿Cómo gestiono la retirada del consentimiento a mitad de sesión?
Haz que la retirada sea inmediata y tan fácil como dar el consentimiento, tal como exige el artículo 7, apartado 3, del RGPD. En la práctica, eso significa un enlace permanente de “Configuración de cookies”, normalmente en el pie de página, que vuelve a abrir las preferencias sin cargar otra página. Cuando un visitante retira una categoría:
- Actualiza el store y guarda la nueva elección.
- Si usas etiquetas de Google, envía
gtag('consent', 'update', …)con las señales correspondientes en'denied'. - Deja de llamar a las funciones de seguimiento; el código condicionado por el store lo hace automáticamente.
- Elimina las cookies propias que instaló la categoría retirada, siempre que conozcas sus nombres y dominios. Tus scripts no pueden eliminar cookies instaladas en otros dominios.
- Recarga la página si algún script retirado ya se está ejecutando. Las bibliotecas mantienen temporizadores, listeners e identificadores en memoria, y recargar es la única forma fiable de eliminarlos.
Recargar tras la retirada es una concesión razonable: ocurre pocas veces, y la alternativa es ejecutar código para el que ya no tienes permiso.
En resumen
En una SPA, el consentimiento es estado: deniega por defecto, comparte un único store, condiciona a él los scripts y los contenidos incrustados, envía páginas vistas solo con consentimiento y haz que la retirada surta efecto al instante. Herramientas como Cookiesh pueden encargarse de gran parte de esta fontanería, con un SDK para React que incluye CookieshProvider y un hook useConsent(), además de un evento consent:updated para todo lo demás.
Este artículo ofrece información general y no constituye asesoramiento jurídico. Para tomar decisiones sobre tu situación concreta, consulta a un profesional cualificado en privacidad.
Preguntas frecuentes
¿Tengo que volver a mostrar el banner de cookies tras cada cambio de ruta?
No. Muéstralo hasta que el visitante elija, guarda esa elección y mantén un enlace permanente a la configuración. Vuelve a preguntar solo cuando tus finalidades o proveedores cambien de forma sustancial o la elección guardada caduque.
¿Puedo guardar la elección de consentimiento en localStorage en lugar de en una cookie?
Ambas opciones funcionan en el navegador, pero la cookie también se envía a tu servidor con cada solicitud, así que las páginas renderizadas en el servidor pueden mostrar el estado correcto desde el primer renderizado. localStorage solo puede leerlo el código del lado del cliente una vez cargada la aplicación.
¿Los componentes de script de los frameworks, como next/script, gestionan el consentimiento por mí?
No. Utilidades como el componente Script de Next.js controlan cuándo se carga un script en relación con la hidratación, no si el visitante lo ha aceptado. Renderízalos de forma condicional según tu estado de consentimiento para que no se añadan a la página antes de una aceptación.
Fuentes
Equipo editorial de Cookiesh
Ingenieros de privacidad y especialistas en cumplimiento normativo que escriben guías prácticas sobre consentimiento, cookies y protección de datos para equipos de producto.