Getting to know React DOM’s event handling system inside out

Eytan Manor
The Guild
Published in
5 min readOct 22, 2018

It all started when I’ve tried to redirect submitted React event handlers into another DOM element. I won’t get into details regarding the use case, but what I did was fairly logical: I’ve redefined the addEventListener() method on the DOM element’s instance, hoping to capture the submitted arguments and do as I wish with them. Unfortunately, it didn’t work…

How come?! How could it be that React handles events without calling the addEventListener() method? After all, it has proven itself to work, across many many applications.

True, but it’s not what you think. First I would like you to take a snapshot of ReactDOM’s implementation. It actually has a comment which explains the entire event handling system:

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.

Source: src/events/ReactBrowserEventEmitter.js:32

At the beginning this is what I saw:

But after debugging a little, going through the stack trace and some of React’s documentation, things are much clearer now. Let’s break it down then, and try to make things simpler.

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.

React uses a single event listener per single event type to invoke all submitted handlers within the virtual DOM. For example, given the following React component:

We will have a single event listener registered on the native DOM for the click event. By running the getEventListeners() method which is available on Chrome dev-tools, we would get the following result:

{click: Array(1)}

Each event-type listener will be ensured per single render cycle, so if we were to define additional event handlers of keydowntype, we would get the following output:

{click: Array(1), keydown: Array(1)}

Source: packages/react-dom/src/client/ReactDOMComponent.js:225

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

For each and every browser, regardless of its implementation, we will have consistent event arguments, as React normalizes them. Whether we use the latest Chrome browser or IE8, the click event arguments will look like so:

  • boolean altKey
  • number button
  • number buttons
  • number clientX
  • number clientY
  • boolean ctrlKey
  • boolean getModifierState(key)
  • boolean metaKey
  • number pageX
  • number pageY
  • DOMEventTarget relatedTarget
  • number screenX
  • number screenY
  • boolean shiftKey

Docs: events:supported-events
Source: packages/react-dom/src/events/SimpleEventPlugin.js:259

Since React is registering a single event listener per multiple handlers, it would need to re-dispatch the event for each and every handler.

Source: EventPluginHub.js:168

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 is a very central component in React’s event handling system. This is what unifies all event plug-ins into a single place, and will redirect dispatched events to each and every one of them. Each plug-in is responsible for extracting and handling different event types, for example, we have the SimpleEventPlugin will handle events which are likely to be implemented across most browsers like mouse events and key presses (source); we also have the ChangeEventPluginwhich will handle the very famous onChange event (source).

Source: packages/events/EventPluginHub.js:168

Synthetic events are React’s normalized event arguments which ensures that there’s consistency across all browsers, and are being generated by the plug-ins. Note that synthetic events are being pooled! Which means that the same object instance is used in multiple handlers, only it is being reset with new properties before each and every invocation and then disposed:

Docs: events:event-pooling
Source: packages/react-dom/src/events/SimpleEventPlugin.js:322

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

As mentioned, each and every event can have multiple handlers, even though each of them is actually being listened once by the real DOM. Accordingly, the relevant “dispatches” which consist of event handlers and their corresponding fiber nodes (nodes in the virtual DOM tree) need to be accumulated for future use.

Source: packages/events/EventPropagators.js:90

The `EventPluginHub` then dispatches the events.

The plug-in hub goes through the accumulated information and dispatches the events, thus invoking the submitted event handlers.

Source: packages/events/EventPluginUtils.js:77

So that’s how that events handling system works in a nutshell. There are few things I would like you to note:

  • Top level event listeners which are registered to the main DOM (window.document) can also be registered to other DOMs, depends on where application container’s at. For example, if the container is adopted by an iframe, then the iframe‘s DOM will be the main event listener; it can also be a document fragment, a shadow DOM, etc. It’s important that you’d be aware of that and know that there’s a slight limitation the events’ propagation.
  • React re-dispatches the events in two phases: one for capturing and the other for bubbling, just like how the native DOM does.
  • The event handling which is done for React Native is different than React DOM’s and you shouldn’t confuse between the two! React is just a library that produces a virtual representation of the view that we would like to render, and React DOM/Native are the bridge between React and the environment that we’re using. This article is relevant for React DOM only!

At the end of the day you’ll still be able to use React, with or without this information, but I think that a vastly used library such as React deserves more attention, especially if you wanna step up your game.

So getting back to what brought me to write this article, if I wanted to redirect the registered by React, all I had to do was redefining the addEventListener() for the DOM, and not the corresponding Node. Of course, overwriting a native method is NOT something that should be done and it’s a very bad practice (*cough cough* Zone.js), but I won’t get into my specific use case as this is a topic for another article.

Update: (November 21st, 2018)

For those who liked this article and how I analyze React’s implementation, I recommend you to read my article about React Hooks and how they work under the hood.

--

--