Emitir Eventos a Medida

Tabla de contenido

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 o con menor esfuerzo, emitir un evento a medida y reaccionar cuando ocurra.

Vamos a ver un caso de uso menos típico, combinándolo 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:

  1. Crear un evento
  2. Disparar el evento
  3. 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 la 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.

Recursos

comments powered by Disqus

Si te ha parecido interesante

Tanto si tienes alguna duda o quieres charlar sobre este tema, como si el contenido o nuestros perfiles te parecen interesantes y crees que pdemos hacer algo juntos, no dudes en ponerte en contacto con nosotros a través de twitter o en el email hola@mamutlove.com