No Code, No Bug

React and JS Events


Associating React and JavaScript Events in the same sentence might sound a bit strange. Because of the way React is designed, it is rare to be in a situation where you need to use native JS events to solve problems inside your React components.

React already provides you with quite some cool tools to bind your components to states, to hydrate them using a unique source of data or to make components interact with each other in a dynamic way.

Most of the time, you can leverage the state API, the Context API or the composition paradigm.

However.

There are some situations out there in the real world of React applications where using the JavaScript events alongside React will come very handy to keep things optimized at the lowest development cost.

And that's what we going to see in this article.

Events

You can skip this part and go directly to the problem to solve if you're already familiar with JS events 🙂.

Let's briefly talk about events and how they are implemented in JavaScript. An event, like its name suggests, is simply an action. It is something that can be triggered by multiple sources: the user clicked on a button, the browser changed its viewport, the internet connection got interupted, a DOM node got mutated, and many more. You can find the exhaustive list if you want to know them all on MDN event docs.

The event that was fired returns a JavaScript object that inherits from the Event interface. With it, you can have a lot of meta data around the event and its context.

The most important part is that you can connect a function to the event, so that the function will be executed at the very moment the event is fired. For exemple, it will allow you to run some code right after a user has clicked on a button.

Good news: we don't need to dig deep into the events API for the next part.

React and Events

First things first, let's talk about the problem to solve. After all, this article wouldn't exist if there was no problem to begin with 🧐.

The problem to solve

Your day's task: to connect some button <MyButton> from a complex and deep-nested table to some component <FarAwayComponent> located far-away outside this React tree.

When clicked, this button should increment a counter rendered inside <FarAwayComponent> that is used in a totally different place than where the button lives.

Let me get this straight: there is something like 20 levels of components and thousand of lines of code that stand between <MyButton> and <FarAwayComponent>. On top of that, the codebase is old and components are somehow tighly coupled to one another. This sounds just like any React applications out there, right ? 🙃

The problem here is not so much about how to implement that connection. You can, for example, leverage React's state and context APIs to make the two components talk to each other. You could then use a custom hook to update <FarAwayComponent> from <MyButton>.

The real problem lives in the optimization of the solution you implement. The real problem here is how to NOT re-render your whole or, at least, a big part of your React application when updating the counter state, or writting in the context.

You wouldn't be facing this problem to begin with if the React tree of the application was designed so that the two components that need to communicate were close enough in the hierarchy.

But in the real world you don't have the time to refactor a whole React application to simply implement a basic feature. So you have no choice but to deal with the existing codebase.

Some solutions you could implement to solve the optimization problem:

  • You could memoize some components to make sure they do not update if they don't need to.
  • You could keep the counter state inside <FarAwayComponent>, and store in some context at the nearest root its setter so that <MyButton> can leverage it.

However, both solutions come with a price. The first solution will make you memoize lots of components, thus increasing the bundle size and may cause edge effects (components relying wrongly on the very fact that they are not memoized). The second solution will still cause an extra update of your whole React tree.

Now, what if take a step back from React for a moment, and see if we can solve this problem from a different approach. What if I told you that you can optimize your solution so that only <FarAwayComponent> will update when the button is clicked ? 😉

Well, surprise: we can. And we can use JavaScript events to achieve so.

The optimized solution

The key is to bind the native JS events to the React component's state, so that when the event is triggered from outside, it performs the actions we want on the local state.

Registering the event

// FarAwayComponent.jsx
import React, { useState } from 'react'

const FarAwayComponent = () => {
  const [count, setCount] = useState(0)

  const increment = () => setCount((prevCount) => prevCount + 1)

  useEffect(() => {
    document.addEventListener('setCounter', increment)
    return () => document.removeEventListener('setCounter', increment)
  }, [])

  return <p>Current count: {count}</p>
}

In the example above, we bind the event setCounter to the increment function which simply, as the name suggests, increments the count state variable.

Firing the event

This part doesn't require lots of code; in fact it's a two-liner solution:

// MyButton.jsx
import React from 'react'

// Instanciate the event we created previously
const counterEvent = new Event('setCounter')

const MyButton = () => {
  return <button onClick={() => document.dispatchEvent(counterEvent)}>click me</button>
}

Clicking on the button will fire the setCounter event (instanciated as counterEvent variable), and that will fire any function that was previously bound to it - increment in our case.

Some thoughts

No mater how far FarAwayComponent and MyButton are from each other in the React tree, this technique enables them to communicate together, and thus opens a potentially bi-directionnal data flow.

Note: there's one restriction though: the two components must be loaded in the same page's HTML document.

Demo

You can find the full source code that implements this solution in the following codesandbox: https://codesandbox.io/s/react-and-js-events-c4ftk

Don't hesitate to play around with it to make it fit to a particular situation.

← Back to main page

Alexandre Le Lain

Alexandre Le Lain ‹@a_lelain

Software Engineer at HelloWork. I write code to fix my code.

You can check out my Github anytime.