Dispatching Custom Events

Table of contents

We are used to listen to events and react to them: I trigger this function when a click happens here or when a user scrolls down or after the resize fired because the viewport has changed. But we are not used to emit our own events.

Sometimes we need to know the current state of an API asynchronus task or if a third-party library has performed an action or we want to communicate different parts of our application or components due to an user interaction need to trigger a secondary effect. For instance, an user clicks on a add to cart button and the badge over the cart icon increments its value by 1. How these parts or components are connected depends on the moment in which this feature araises, it can vary if more than one components need to know about the global state or it depends on the stack your are using. A solution could be having a global state and make those components subscribe to it or effortlessly, emit a custom event and react to it.

We are going to review a atipical use case, in combination with React and GSAP.

Knowing React nor GSAP is needed to understand the example nor the article but it will make the code clearer.

Starting point

We have two full-width siblings components, one above the other.

<Parent>
	<ComponentA />
	<ComponentB />
</Parent>

In this case <ComponentB /> is pinned when it reaches the top of the viewport and then runs a nice animation ✨ So in the initial load of the document <ComponentB /> is going to calculate when it will reach the top to anchor and start its animation, taking advance of its position in the document. But of course, before it reaches the top the user clicks on <ComponentA /> and changes its content (adding or deleting) so its height is different and consequently changes the current position of <ComponentB /> within the document, making the initial calculations no longer match the current reality 🪄.

Solutions

As I said before, we could create a context and subscribe to it so we will know when that click ocurrs, or set a global state or useResizeObserver.

But if we use ResizeObserver this article should be named “how to use ResizeObserver” and it is not, so we will see how to work with custom events. Furthermore, using ResizeObserver we will know that <ComponentA /> has changed but <ComponentB /> still be unaware of that :P

Custom Events

There are three steps:

  1. Create an event
  2. Dispatch the event
  3. Listen and react to the event

Create an event

Using the constuctor CustomEvent we can create an event. It expects two parameters being the second optional.

const event = new CustomEvent(type[, options]);

The first parameter is a string and it specifies the name (type) of the event, the second one is an object and it is useful to set the options the event needs. In addition to the options available from Event we can use the property detail which it is very useful to attach key information to the event itself.

const componentHasChanged = new CustomEvent('refreshScrollTrigger', {
	bubbles: true, // From Event. Makes the event bubble through the DOM
	detail: {
		count: 1,
	},
})

Dispatch the event

Once the event is created, we need to dispatch it (like in the novel Contact by Carl Sagan) so other components can listen and react to it.

The event can be directly dispateched.

element.dispatch(componentHasChanged)

Or associated to another event.

element.dispatch('click', (event) => element.dispatch(componentHasChanged))

Or depending if a property changes

useEffect(() =>
  element.dispatch(componentHasChanged);
[content]);

Listen and react to the event

We are going to use addEventListener method passing it the name of the event and the callback that will be the function that will refresh the calculation of its position.

// The animation
const tl = gsap.timeline(/*...*/).to(/*...*/)

// The function that updates the the position that the element occupies
const updateScrollTrigger = () => tl.scrollTrigger.refresh()

// The mechanisms to listen to the event
window.addEventListener('refreshScrollTrigger', updateScrollTrigger)

If we need to access to the detail property we do straight trought the event:

const updateScrollTrigger = (event) => {
	if (event.detail.count > 0) {
		tl.scrollTrigger.refresh()
	}
}

CustomEvent vs Event

Could we used the Event constructor instead of the CustomEvent one? Yes, we can consider them interchangeable since CustomEvent is based on Event but it is worth remembering that CustomEvent can use the detail property to add information along with the event, which could be anything, such as the value of an attribute.

const event = new CustomEvent('awesomeEvent', {
	detail: element.dataset.title,
})

On the other hand, it makes sense to use Event for standard envet types and CustomEvent for those tailored or custom.

A note of caution

Earlier we talked about other possible solutions and one of the reasons for not choosing CustomEvent is that since through these events are triggering functions that have side effects, we are hiding a little bit the need for some parts to communicate, which is a little tricky and due to we could be hiding, for example, that our componentization has not been correct*.*

Conclusion

Knowing all the features your application needs beforehand allows you to make smarter decisions. Maybe you don’t need a very big, structured and solid solution but something small, flexible and exceptional. It also happens from time to time that the product is very advanced and it is “too late” to add new features 🤷 Knowing when it is convenient to use one tool or another is a knowledge that from the outside seems innate but often is the result of practice. So practice and try all the new solutions you can think of, because that’s where creativity lives.

Resources

comments powered by Disqus

If you find it interesting

If you have any doubt or you want to talk about this topic, if the content or our profiles are interesting to you and you think we could something together, do not hesitate to contact us on twitter or trough the email address hola@mamutlove.com