Estamos acostumbrados a escuchar eventos y reaccionar a ellos: ejecuto una
función cuando ocurre un click aquí, o cuando un usuario hace scroll allá o
se dispara un resize porque el viewport ha cambiado. Pero quizá no lo
estamos tanto a emitir nuestros propios eventos.
A veces necesitamos saber el estado de una tarea asíncrona de una API, si una
librería de terceros ha realizado una acción o queremos comunicar partes
diferentes de una web, aplicación o componentes porque una acción de un usuario
tiene que disparar un efecto secundario en otro lugar. Por ejemplo, un usuario
hace click en un botón para añadir un producto al carrito y el contador sobre
el icono del carrito actualiza su valor sumándole 1. Cómo estas partes o
componentes se comunican puede estar establecida de diferentes formas
dependiendo del momento en el que surge la necesidad, de si van a haber más
componentes dependientes de un estado global o del stack que estés utilizando.
Una solución podría ser tener un estado global y subscribir algunos componentes
a éste, un contexto o con menor esfuerzo, emitir un evento a medida y
reaccionar cuando ocurra.
Vamos a ver un caso de uso menos típico, combinando con React y GSAP.
No hace falta saber React ni GSAP para entender el ejemplo o el artículo pero es más claro si conoces un poquito de ambas librerías.
Punto de partida
Tenemos dos componentes hermanos que ocupan todo el ancho de la pantalla y por lo tanto uno está encima del otro.
<Parent>
<ComponentA />
<ComponentB />
</Parent>
Se da la particularidad de que <ComponentB /> se ancla (se pinnea) cuando
alcanza la parte superior del viewport y a continuación ejecuta una bonita
animación ✨ De modo que en la carga inicial del documento <ComponentB /> va a
calcular cuándo va a alcanzar la parte superior para anclarse e iniciar su
animación, partiendo de su posición en el documento. Pero claro, antes de que
llegue arriba el usuario hace un click en <ComponentA /> y cambia su
contenido (poniendo y quitando) de modo que su altura es diferente y en
consecuencia cambia la posición de <ComponentB /> dentro del documento,
haciendo que los cálculos iniciales ya no coincidan con la realidad actual 🪄
Posibles soluciones
Como dijimos, podríamos crear un contexto y subscribimos para saber cuándo ese
click ha ocurrido, o tener un estado global de la aplicación, o podemos usar
ResizeObserver.
Pero si usásemos ResizeObserver este artículo se llamaría “cómo usar
ResizeObserver” y como no es el caso pues vamos con los eventos a medida.
Además, sabríamos que <ComponentA /> ha cambiado pero <ComponentB />
seguiría sin saberlo :P
Custom Events
El mecanismo tiene tres pasos:
- Crear un evento
- Disparar el evento
- Escuchar y reaccionar al evento
Crear un evento
Utilizando el constructor CustomEvent podemos crear un evento. Espera dos
parámetros aunque el segundo es opcional.
const event = new CustomEvent(type, options);
El primero es un string y sirve para indicar el nombre (tipo) del evento y el
segundo es un object y sirve para detallar las opciones del evento. Además de
todas las opciones disponibles para Event, puede usar detail que es muy útil
para enviar información asociada al evento y que queramos conocer al otro lado.
const componentHasChanged = new CustomEvent('refreshScrollTrigger', {
bubbles: true, // Proviene de Event. Permite que el evento se propague
detail: {
count: 1,
},
});
Disparar el evento
Una vez creado, necesitamos enviarlo para que pueda ser escuchado (como en Contact de Carl Sagan) y ejecutar una función después.
Se puede disparar directamente
element.dispatch(componentHasChanged);
O vinculado a otro evento.
element.dispatch('click', (event) => element.dispatch(componentHasChanged));
O si una propiedad cambia
useEffect(() =>
element.dispatch(componentHasChanged);
[content]);
Escuchar y reaccionar al evento
Vamos a usar addEventListener pasándole el nombre del evento y el callback
que será la función refrescará el cálculo de su posición.
// La animación
const tl = gsap.timeline(/*...*/).to(/*...*/);
// La función que actualiza el conocimiento sobre la posición que el elemento ocupa
const updateScrollTrigger = () => tl.scrollTrigger.refresh();
// El mecanismo para escuchar el evento
window.addEventListener('refreshScrollTrigger', updateScrollTrigger);
En el caso de necesitar acceder a detail podemos hacerlo directamente desde el
evento:
const updateScrollTrigger = (event) => {
if (event.detail.count > 0) {
tl.scrollTrigger.refresh();
}
};
CustomEvent vs Event
¿Podríamos haber utilizado Event en lugar de CustomEvent? Sí, hasta cierto
punto podemos considerarlos intercambiables ya que CustomEvent se basa en
Event pero conviene recordar la diferencia de que CustomEvent puede usar la
propiedad detail para añadir información junto con el evento, que podría ser
cualquier cosa, como el valor de un atributo.
const event = new CustomEvent('awesomeEvent', {
detail: element.dataset.title,
});
Por otro lado, tiene sentido que usemos Event para los eventos conocidos y ya
que ya están implementados y conoce el navegador y CustomEvent para los que
sean a medida.
Una nota de precaución
Antes hablábamos de otras posibles soluciones y es que uno de los motivos para
no elegir CustomEvent es que como a través de estos eventos estamos disparando
funciones que tiene efectos secundarios, estamos escondiendo un poco la
necesidad de que algunas partes se comuniquen, lo cuál es un poco tricky y
podríamos estar escondiendo, por ejemplo, que nuestra componentización no ha
sido adecuada.
Conclusión
Conocer todas las necesidades de lo que estás construyendo con antelación te permite tomar mejores decisiones, y tal vez no necesitas una solución muy grande, estructurada y sólida sino algo pequeño, flexible y excepcional. También ocurre de vez en cuando que el producto está muy avanzado y es “demasiado tarde” para añadir nuevas funcionalidades 🤷 Saber cuándo conviene usar una herramienta u otra es un conocimiento que desde fuera parece innato pero a menudo es el resultado de la práctica. Así que practica y prueba todas las soluciones nuevas que se te ocurran, ahí vive la creatividad.
