Events in React: What Do They Do? Do They Do Things?? Let’s Find Out!

Asís García
Jun 24 · 8 min read
Photo by on

In this story I’ll try to explain the way React handles events (as of ). The first part of the story is focused in the implementation details. I’ll try to explain the inner workings of the library based on my findings reading its source code. In the second part, I’ll talk about how those implementation details might affect the way you handle events in your applications.

Motivation for this story

Some weeks ago, while conducting a training session with a team of developers new to React, I was talking about synthetic events and their nuances when one of the attendants asked me a question about the way React handles events, internally. I then realized that, while from a library-user point of view I’m really confident on my knowledge, I don’t really know about the implementation details.

So here we are. In this story I’ll share with you my findings while trying to get a deeper understanding of events and event handling in React applications.

The source code

The best way to understand the inner workings of some library is by reading its code. When dealing with a library such big and complex as React, this can be a daunting task.

There is a lot going on when React renders an application into the DOM. In order to be able to find my way through the source code, I had to read some very high-level (and hopefully not totally outdated) descriptions of and the , and a very low-level from Max Koretskyi aka Wizard.

A hidden gem

After some hours (yes, hours 😅) tinkering with a minimal example, setting up some breakpoints and jumping around the source code, I stumbled upon a comment . The comment (reproduced below with some format adjustments for the sake of readability) describes the system used to handle events in React applications. Finding it was such a win. Instead of having to slowly discover the architecture behind event handling “all by myself”, there it was briefly outlined in five simple points. I’ll try to explain each of the points in the following sections.

Summary of `ReactBrowserEventEmitter` event handling:

- Top-level delegation is used to trap most native browser
events. This may only occur in the main thread and is
the responsibility of ReactDOMEventListener, which is
injected and can therefore support pluggable event
sources. This is the only work that occurs in the main
thread.

- We normalize and de-duplicate events to account for
browser quirks. This may be done in the worker thread.

- Forward these native events (with the associated
top-level type used to trap it) to `EventPluginHub`,
which in turn will ask plugins if they want to extract
any synthetic events.

- The `EventPluginHub` will then process each event by
annotating them with "dispatches", a sequence of
listeners and IDs that care about that event.

- The `EventPluginHub` then dispatches the events.

Top-level delegation

React handles (most of) the events in your application using a very common technique: . When you render something like:

<button onClick={() => alert("clicked!")}>click me</button>

React doesn’t attach a DOM event listener to the button node. Instead, it gets a reference to the DOM document root where it is rendering the component, and a DOM event listener there. In fact, React installs a single top-level event listener for each type of event. This happens when a component is mounted or updated.

Whenever a DOM event is fired, those top-level listeners initiate the actual event dispatching through the React application.

Synthetic events

React defines as a common interface which ”implement(s) the DOM Level 3 Events API by normalizing browser quirks”. These are the kind of events React applications produce and handle.

SyntheticEvents use a pooling mechanism (you can see its actual implementation ) which has its own implications when dealing with them in your application (for more details on this you can read , a story by my colleague Ceci García García).

Handling a native event

Up until now, React has rendered your application to the DOM, setting some event listeners in the document root. And then something happens (say, the user clicks some button) and a native event is triggered. This will fire the top-level responsible for handling that event. This is a :

  1. First, extracting synthetic events from the native event.
  2. After that, handling the extracted events.

Extracting the synthetic events

How does React get from a native event to its synthetic counterpart? That’s the responsibility of the and its event plugins. Each plugin is a module (like or ) that knows which synthetic event(s) to produce in response to a native event.

When a native event is fired, React hands it over in the EventPluginHub. The plugin then uses different information (like the event’s name or its DOM target) to some synthetic event(s).

Handling the synthetic events

Before we continue, I want you to focus for a second on the following example:

If you click the div, you get this in the console:

inner
outer

Remember that React is handling every click with a single event listener attached to the document root. There is no event listener attached to the individual div nodes in the DOM. So, how does the code above even work? How does React know that it has to invoke both of the callbacks, and in that order?

Well, it turns out that, after extracting a synthetic event, each event plugin also the list of event handlers to be invoked in response to that synthetic event (the list of “dispatches” in the source code jargon).

The plugins use a to traverse the React components hierarchy corresponding to the native event target. For each component in the hierarchy, React looks for event handlers to call in the capturing and bubbling phases. When it finds an event handler, it “enqueues” a new dispatch.

Once all the synthetic events have been extracted and their respective list of dispatches computed, it’s time to . For each synthetic event, the list of its dispatches is iterated and the corresponding event handler executed (that is your code, at last! 😅), the currentTarget property to the right DOM node. React simulates event propagation control by whenever the event’s stopPropagation method is called.

After executing the event dispatches, if the event was not persisted, it’s .


React event handling specifics

Now that we know how React “adapts” the native event system, we can finally understand the underlaying causes regarding some of the specifics about event handling in React applications.

Immediate propagation

When working with a DOM node, you can attach to it multiple event listeners for the same event:

const button = document.getElementById("myButton");button.addEventListener("click", () => alert("handler 1"));
button.addEventListener("click", () => alert("handler 2"));

But React elements accept a single prop, so you can only set one event handler:

<button onClick={() => alert("a single handler")}>...</button>

This is why synthetic events don’t expose a method: the concept of immediate propagation of synthetic events makes no sense.

Propagation context

Synthetic events “regular” propagation can be stopped, though. This works like a charm while all your event handlers are executed in the context of the React application. But if you mix React event listeners with native ones, things can get tricky.

In the following example we use a custom hook to simulate the usage of a vanilla JavaScript library which adds some event listener to the DOM:

If you run this application and click the button, you will see this in the console:

React event handler
vanilla JS lib event handler

As you can see, stopping the event propagation in the React “world” (calling stopPropagation on the synthetic event) does not prevent the global handler from executing. Why? Well, the global handler is installed in the top-level, just like the React top-level event handler. To prevent its execution you must call stopImmediatePropagation on the native event!

With this change (line 17 in the code snippet), everything works as expected and you get this in the console after clicking the button:

React event handler

Event propagation through portals

let you render part of your application “into a DOM node that exists outside the DOM hierarchy of the parent component”.

The documentation on portals has . It says that:

An event fired from inside a portal will propagate to ancestors in the containing React tree, even if those elements are not ancestors in the DOM tree.

This way your components can capture events bubbling from their children regardless of where they are rendered in the DOM.

I’ve always wondered how this works internally. Now that I know how React event propagation, I understand what’s happening: portals are React components which happen to render in different containers in the DOM, but their position within the React tree is the same as that of a “normal” component. And the capture-and-bubble algorithm used to simulate event propagation only cares about the React tree.

target vs currentTarget

DOM native events define two important properties:

  • : the node firing the event. This property lets you implement .
  • : the node the event handler is attached to. This property lets you handle the same event in different nodes using the same event handler.

React’s synthetic events also expose these two properties, which you can use in the same way as their native counterparts. React takes care of setting target and currentTarget to the right DOM node:

The way React handles the currentTarget, though, has implications in the native-synthetic equivalence: while the target property of both the native and the synthetic event points to the same DOM node, currentTarget doesn’t.

For a synthetic event, currentTarget will point to the DOM node corresponding to the React component handling the “delegated” event. But the native event will have currentTarget pointing to the document root! This is normal, as React is handling the application events in the top-level (using event delegation, in fact).

Parting notes and future work

While most of the applications will never have to deal with the subtleties discussed in this story, I myself have struggled many times before with these details while trying to solve event-related issues. Had I known back then about the way React implements the event system, I’d have avoided some random stopPropagation invocations 😅.

I’ve found this deep dive into the source code of React to be really challenging, but also really interesting. Thankfully, the official React documentation is an amazing resource to learn about the library, so you don’t need to read a single line of the source code in order to use it to build amazing stuff. But it’s always fun being able to learn about how things work if you want to. That’s the magic of open source 😄.

And now, if you enjoyed this story:

😄

Trabe

We are a development studio. We use Java, Rails, and JavaScript. This is where we write about the technologies we use at Trabe.

Thanks to Ceci García García and David Barral.

Asís García

Written by

Developer @Trabe

Trabe

Trabe

We are a development studio. We use Java, Rails, and JavaScript. This is where we write about the technologies we use at Trabe.