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

Asís García
Trabe
Published in
8 min readJun 24, 2019
Photo by Bill Oxford on Unsplash

In this story I’ll try to explain the way React handles events (as of version 16.8.6). 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 React and the Fiber architecture, and a very low-level series on React Fiber 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 in this file . 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: event delegation. 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 installs 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 SyntheticEvents 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 in the source code) which has its own implications when dealing with them in your application (for more details on this you can read React SyntheticEvent reuse, 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 DOM event listener responsible for handling that event. This is a process with two steps:

  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 EventPluginHub and its event plugins. Each plugin is a module (like SimpleEventPlugin or ChangeEventPlugin) that knows which synthetic event(s) to produce in response to a native event.

When a native event is fired, React hands it over to each plugin in the EventPluginHub. The plugin then uses different information (like the event’s name or its DOM target) to extract 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 computes 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 capture-and-bubble simulation 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 run the event handlers. For each synthetic event, the list of its dispatches is iterated and the corresponding event handler executed (that is your code, at last! 😅), setting the currentTarget property to the right DOM node. React simulates event propagation control by stopping the iteration whenever the event’s stopPropagation method is called.

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

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 stopImmediatePropagation 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

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 a specific section about event bubbling. 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 implements 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:

  • target: the node firing the event. This property lets you implement event delegation.
  • currentTarget: 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 😄.

--

--