wandres.dev
VIEW TRANSITIONS · ClientRouter y transiciones

Los eventos del ciclo: reaccionar a cada navegación

La navegación con el ClientRouter no es un salto opaco, sino un ciclo con etapas que emite eventos sobre document. Qué significan astro:before-preparation, astro:after-swap y astro:page-load; dónde encaja astro:before-swap; el problema clásico de que los scripts de módulo no se re-ejecutan al intercambiar el DOM, y cómo astro:page-load es el gancho para re-inicializar todo lo que debe correr en cada página.

⏱ 17 min

La navegación intervenida del <ClientRouter /> parece instantánea, pero por dentro es un pequeño proceso con etapas: preparar la página siguiente, intercambiar el DOM, dejarla lista. Astro no esconde ese proceso; lo publica como una secuencia de eventos que se disparan sobre document, y engancharse a ellos es la forma de ejecutar tu propio código en el momento exacto del ciclo que te interesa. Este es también el capítulo donde se resuelve un desconcierto muy común: por qué un <script> que funcionaba con recargas deja de ejecutarse cuando activas las view transitions. La respuesta —y su solución— viven en estos eventos.

🎯 Al terminar esta lección sabrás
  • Situar la navegación como un ciclo con etapas y no como un salto opaco.
  • Distinguir astro:before-preparation, astro:before-swap, astro:after-swap y astro:page-load.
  • Entender por qué los scripts de módulo no se re-ejecutan al intercambiar el DOM.
  • Usar astro:page-load para re-inicializar en cada navegación y astro:after-swap para el estado previo al pintado.

La navegación como un ciclo con etapas

Cuando pulsas un enlace con el router activo, no ocurre un único evento sino una cadena. Primero Astro prepara: decide cargar la página destino y trae su HTML. Luego intercambia: reemplaza el DOM actual por el nuevo dentro de la view transition. Por último la deja cargada: la transición termina y la página está lista para vivir. Cada frontera entre esas etapas emite un evento sobre document, de modo que puedes intervenir antes de preparar, justo antes o después del intercambio, o cuando todo ha terminado.

document.addEventListener('astro:before-preparation', (event) => {
  // arranca la navegacion: aun no se ha cargado la pagina destino
});
document.addEventListener('astro:after-swap', () => {
  // el DOM nuevo ya esta, pero la transicion aun no ha terminado
});
document.addEventListener('astro:page-load', () => {
  // la pagina esta cargada y lista; tambien corre en la carga inicial
});

before-preparation: el arranque de la navegación

astro:before-preparation es el primer eslabón: se dispara al iniciarse la navegación, antes de que la página destino se haya cargado. El objeto del evento trae información útil —de dónde vienes, a dónde vas, la dirección del salto y el tipo de navegación— y expone el cargador que traerá la página, que puedes incluso reemplazar para casos avanzados. Su uso más honesto y frecuente es mostrar un indicador de carga: como aquí sabes que empieza un viaje que puede tardar, es el momento de encender una barra de progreso, que apagarás cuando la página esté lista.

document.addEventListener('astro:before-preparation', () => {
  document.body.classList.add('cargando');
});
document.addEventListener('astro:page-load', () => {
  document.body.classList.remove('cargando');
});

Ese indicador se apoya en la simetría del ciclo —enciendes al principio, apagas al final—, pero el objeto del evento ofrece más que un simple disparador: describe el viaje. De él puedes leer de dónde vienes, a dónde vas y, sobre todo, la dirección del salto —hacia delante o hacia atrás—, un dato que permite adaptar la interfaz al sentido de la navegación.

document.addEventListener('astro:before-preparation', (event) => {
  // 'forward' al avanzar, 'back' al pulsar atras
  document.documentElement.dataset.direccion = event.direction;
});

Con ese dato escrito en un atributo del documento, tu CSS puede animar distinto al avanzar que al retroceder, replicando a mano la sensibilidad direccional que slide trae de fábrica. El evento no solo te avisa de que algo empieza: te cuenta qué clase de movimiento es, y esa información es materia prima para transiciones que se sienten coherentes con el gesto del usuario.

after-swap y before-swap: el instante del trasplante

Entre la preparación y el final está el momento crítico: el intercambio del DOM. Dos eventos lo rodean. astro:before-swap se dispara justo antes, cuando la página nueva ya está construida pero todavía no ha sustituido a la vieja; su objeto de evento da acceso al documento entrante y permite incluso personalizar cómo se hace el intercambio. astro:after-swap se dispara justo después, cuando el DOM nuevo ya está en su sitio pero la transición aún no ha concluido.

Ese astro:after-swap tiene un uso muy concreto y valioso: fijar estado que debe estar puesto antes de que el usuario vea la página, para evitar un parpadeo. El caso de libro es el tema claro/oscuro. Si lees la preferencia del usuario y aplicas la clase en astro:page-load, corres el riesgo de que la página se pinte un instante con el tema por defecto y luego cambie. Haciéndolo en astro:after-swap, la clase ya está antes del primer pintado.

document.addEventListener('astro:after-swap', () => {
  const tema = localStorage.getItem('tema');
  if (tema === 'oscuro') {
    document.documentElement.classList.add('oscuro');
  }
});

Su gemelo astro:before-swap es más avanzado y menos habitual, pero abre una puerta que conviene conocer. Como recibe el documento entrante antes de que sustituya al actual, permite inspeccionarlo y retocarlo —trasladar al nuevo <html> un atributo del viejo, conservar una clase de estado que se perdería en el intercambio— e incluso reemplazar por completo la mecánica del trasplante a través de un método que el propio evento expone. Es el punto donde el desarrollador que necesita controlar el intercambio hasta el último detalle toma las riendas. La mayoría de las veces no harás nada aquí y astro:after-swap bastará; pero saber que before-swap existe te da una salida cuando el comportamiento por defecto, por una vez, no es el que necesitas.

💡
after-swap para lo que debe estar antes del pintado

La regla para elegir entre astro:after-swap y astro:page-load es sencilla: si lo que haces afecta a cómo se ve la página en su primer fotograma —tema, dirección del texto, una clase que evita un destello— hazlo en astro:after-swap, que corre antes de que la transición pinte. Si lo que haces puede esperar a que la página esté del todo lista —montar un widget, medir, escuchar clics— hazlo en astro:page-load. Uno es para el “antes de verse”; el otro, para el “ya está viva”.

El problema de los scripts que dejan de correr

Aquí está el desconcierto que mencionábamos. En Astro, un <script> normal es un módulo, y los módulos tienen una regla del lenguaje: se ejecutan una sola vez, la primera vez que se cargan. En la web multipágina clásica eso no se notaba, porque cada recarga era una carga nueva y el script volvía a correr. Pero con el <ClientRouter /> no hay recarga: se intercambia el DOM. El módulo ya se ejecutó una vez y no se vuelve a ejecutar aunque su <script> aparezca en la página nueva. Por eso el código que inicializa algo —un carrusel, un menú, un listener— funciona en la primera carga y misteriosamente deja de funcionar al navegar.

La solución no es un truco, es un cambio de mentalidad: el código que debe correr en cada página no puede confiar en la ejecución única del módulo; tiene que engancharse a astro:page-load, que se dispara en cada navegación —y también en la carga inicial—. Ese doble disparo es deliberado y perfecto: te permite escribir la inicialización una vez y que corra siempre, tanto al entrar por primera vez como en cada salto posterior.

function inicializarMenu() {
  const boton = document.querySelector('.menu-boton');
  boton?.addEventListener('click', () => {
    document.querySelector('.menu')?.classList.toggle('abierto');
  });
}

// se ejecuta en la carga inicial y en cada navegacion posterior
document.addEventListener('astro:page-load', inicializarMenu);
⚠️
No inicialices en el cuerpo del módulo; hazlo en page-load

Si escribes la inicialización directamente en el cuerpo de un <script>, corre una vez y no vuelve. Con view transitions eso significa que todo lo que dependa de ese arranque se rompe en cuanto el usuario navega. La disciplina es envolver la inicialización en una función y llamarla desde astro:page-load. Y ojo con duplicar listeners globales: si añades uno sobre window o document en cada page-load, se acumulan salto tras salto. Para esos, usa astro:before-preparation o comprueba que no lo has añadido ya.

Puestos en orden, los cuatro eventos cuentan la historia completa de una navegación: astro:before-preparation al arrancar, astro:before-swap cuando la página nueva está lista para entrar, astro:after-swap en cuanto ha entrado, y astro:page-load cuando todo ha terminado. Entre medias, Astro emite también astro:after-preparation al acabar de cargar el destino, de modo que la secuencia es en realidad una escalera fina de peldaños que puedes pisar según la precisión que necesites. Dominar en qué punto de esa línea temporal corre tu código es dominar las view transitions a fondo: deja de haber magia, y en su lugar hay una secuencia clara en la que sabes exactamente dónde colgar cada cosa —el indicador al arranque, el estado sin parpadeo antes del pintado, la inicialización al final—.

flowchart LR
A[clic en un enlace] --> B[astro before-preparation]
B --> C[carga del HTML destino]
C --> D[astro before-swap]
D --> E[intercambio del DOM]
E --> F[astro after-swap]
F --> G[fin de la transicion]
G --> H[astro page-load]
style B fill:#89b4fa,color:#11111b
style F fill:#f9e2af,color:#11111b
style H fill:#a6e3a1,color:#11111b
🚦

before-preparation

Arranca la navegacion antes de cargar el destino. Ideal para encender un indicador de carga.

🎨

after-swap

El DOM nuevo ya esta pero aun no se pinto. Fija aqui el tema para evitar el parpadeo.

page-load

La pagina esta lista, y corre tambien en la carga inicial. El gancho para re-inicializar scripts.

🔁

Modulos, una sola vez

Un script de modulo no se re-ejecuta al intercambiar el DOM. Mueve su init a page-load.

Los eventos no son una API que aprender, sino el ciclo que ya existía hecho visible

Es fácil ver esta lista de eventos como una interfaz más que memorizar, pero eso pasa por alto lo que de verdad ofrecen. Lo que Astro hace al publicarlos es tomar un proceso que siempre existió —cargar, intercambiar, dejar listo— y volverlo observable, convertir en puntos de enganche nombrados unas fronteras que antes eran invisibles porque una recarga las atravesaba todas de golpe. Y al hacerlo revela una verdad incómoda que la recarga escondía: en la web clásica creíamos que un <script> “corre cuando se carga la página”, pero esa frase mezclaba dos cosas que la recarga mantenía pegadas —la carga del documento y la ejecución del código— y que las view transitions, al separar la navegación del recargar, por fin distinguen. El módulo que deja de correr no es un defecto de Astro: es la regla del lenguaje saliendo a la luz en cuanto dejamos de recargar. Por eso la solución no es pelear contra el sistema sino pensar en el vocabulario correcto: dejar de preguntar “cuándo se carga la página” y empezar a preguntar “en qué etapa del ciclo debe correr esto”. Esa reformulación es todo el aprendizaje. El estado que evita un parpadeo pertenece al antes-del-pintado; la inicialización de un widget, al ya-está-lista; el indicador de progreso, al arranque. Cuando internalizas que una navegación es una línea temporal con etapas nombradas, y no un instante mágico, dejas de escribir código que “a veces funciona” y empiezas a colocar cada efecto en el único punto donde tiene sentido. Los frameworks maduros no te dan más magia: te dan menos, y a cambio te enseñan la máquina.

⚔️ Coloca tu código en la etapa correcta
  1. Escribe una inicialización de un menú directamente en el cuerpo de un <script> y comprueba que se rompe al navegar; luego muévela a astro:page-load y verifica que ya funciona en cada salto.
  2. Enciende una clase cargando en astro:before-preparation y apágala en astro:page-load para dibujar un indicador de progreso durante la navegación.
  3. Aplica el tema oscuro desde localStorage en astro:after-swap y confirma que no hay parpadeo del tema por defecto al cambiar de página.
  4. Añade a propósito un listener global en cada astro:page-load, observa cómo se duplica salto tras salto y corrige el patrón para que no se acumule.