The React and React Native Event System Explained: A Harmonious Coexistence

You’re using it. You’re liking it. But did you know what React’s event handler is doing under the hood?

Nicolas Couvrat
Level Up Coding

--

Stuff can sometimes get surprisingly messy if you don’t know how it works…

There are an awful lot of posts explaining how to use React’s event handling system, but not many that explain how it works. I have been working on React Native lately, and my struggles with event handling acted as a reminder of how important it was to understand precisely what’s going on. I thus decided to gather as much info as possible regarding event handling in React: the following is a report of what I found looking around the source code.

Let’s go!

Event handling in React: An overview

Conceptually speaking, event handling in React is nothing revolutionary. Its only goal is intercepting various events (clicks, touches…) and triggering the associated callbacks that you, the programmer, coded. It’s the implementation that makes React’s event handling system stand out.

An overview of React’s event handling flow

One thing React emphasizes is harmonization: cross-browser for React web, cross-platform for React Native. But the event system actually takes this concept one step further by having an (almost) identical event processing system for both React web and React Native. That’s right: both DOM and native events are treated using — minus a little bit of pre-processing —the exact same code. How does React pull off this magic trick? This could be the subject of an article — if not several — in itself, so let’s try to be brief.

Welcome to the magical world of Fiber

What happens when an app updates (let’s say, after clicking a button)? New information propagates, and the app has to be rendered again with it. Now, the core idea behind React (web or native) is to cut this very process into two separate phases: “reconciliation” — where React calculates differences and decides what updates are needed — and “rendering” — where the updates are actually applied. See where that leads us? You’re right. The “reconciliation” phase does not care about how or where the rendering is done, only about what should be rendered. Consequently, the same process can be used for both React Native and React web. The only remaining task to be done is plugging in the appropriate rendering engine.

Event processing is part of the “reconciliation” phase, and thus take place in the same abstract world, where browser events and DOM components are no different from native events and components. What does that world look like? It might be a little complex to picture, since we are so used to thinking in terms of visible, tangible objects, but in this parallel universe, each and every component becomes a Fiber . Indeed, as the React reconciliation algorithm does not care about how components are rendered but only about what changed between two render iterations, components themselves do not matter. Only the work that has to be done to go from the previous state of the component to the new state matters (that work can also be no operation, if no changes happened). And that’s what a Fiber is: not a physical entity but a unit of work, a small step in the grand scheme of the reconciliation process.

For those whose curiosity has been spiked by the previous introduction to Fibers, I advise you to learn more on Fiber and React Fiber! This fun video presentation by Lin Clark is a good start. For everyone else, do not worry: understanding Fiber is absolutely not required to grasp the rest of this article (the switch to Fiber is rather recent, and the event management system did not undergo any major changes in the process anyway). The thing to remember is this: React works in an “abstract world” where updates are made independently of the physical representation of the component: the multiple “real worlds” (the browser, your phone…) where components are rendered are but projections of that unique, device-independent universe. Event handling is no different, and pretty much everything happens in this “abstract world” —whether the event initially came from the DOM or from native, it does not matter.

In the case of event handling, the “listening, normalizing, & re-emitting” phase exists precisely for the purpose of transforming real events and components into their abstract counterparts. It captures native events coming from components, and turns them into what React calls a topLevelType associated with a Fiber . As a result, native events and components themselves are effectively invisible to the downstream event processing system, and no handlers are installed in the “real” environment: everything takes place in the virtual DOM.

Receiving (listening to) events

Alright, looking at the above drawing, it seems that in every case the event handling starts with a listening phase. This is little surprising. After all, many of us are used to having to define our own custom listeners in our applications — because we want it to react only to click and not mousescroll for example. But why would React itself need to listen to all events? It’s becase events appear in their “natural” environment: the DOM for web applications, and native on your mobile device. React, be it in its web or native flavor, is a tool built on top of these fundamental environments. As a result, events do not naturally go through React, and it has to actively listen to them.

Receiving events: React web

For React web, the process is fairly simple and uses top-level delegation. This means that React listens to every event at the document level, which has an interesting implication: by the time any React related code is executed, events have already gone through a first capture/bubbling cycle across the DOM tree.

After receiving that event from the browser, React performs an additional cross-browser harmonization step. As a workaround for browsers having different names for what is effectively the same event, React defines topLevelTypes that are wrappers around browser-specific events. For instance, transitionEnd , webkitTransitionEnd , MozTransitionEnd and oTransitionEnd all become topAnimationEnd — effectively alleviating part of the pain of designing cross-browser applications via consolidation.

Receiving events: React Native

For React Native, events are received over the bridge that links native code with React. In short, whenever a View is created, React also passes its ID number over to native, so as to be able to receive all events related to that element. Again, slight modifications are performed before passing the (touch) event downstream, including adding the touches and changedTouches arrays to the event in order to make it W3 compliant.

From now on, in order to differentiate them for the SyntheticEvents that will be introduced later, we will refer to what we have called “events” (i.e. event objects, coming from either native or the browser, that underwent slight modifications) as “native events.”

The innards of React’s event management system

We now have our native events, harmonized across platforms and browsers. Great! We are now ready to start the real work: passing these events to the appropriate callback(s). Such is the duty of React’s event system. Let’s take a closer look.

Flow of events inside React’s event system

Whew, there’s quite a lot of stuff everywhere. Still, EventPluginHub and its event plugins stand out of the lot. EventPluginHub is in fact the keystone of the entire system, as it:

  • Provides a unified interface for event plugins to be injected into.
  • Runs through the injected plugins every time a new native event is received, collecting the SyntheticEvents returned before dispatching them all.

On the other hand, event plugins all have a similar structure and take native events as inputs, outputting one or several SyntheticEvents , complete with an array of dispatches (functions) to be executed at a later stage. SyntheticEvent is a React specific wrapper around native events, that essentially have the same interface as the browser events you’re already used to, including stopPropagation() and preventDefault() (for more information, the official documentation on events has a dedicated page here).

Event plugins

Although there is a wide variety of different plugins for events, including SimpleEventPlugin (that handles onClick , onTouch, etc.) and the famous ResponderEventPlugin, they all follow the same pattern:

  1. Create one or more SyntheticEvents in response to native events.
  2. Collect all dispatches (i.e. the functions provided by you, the coder) associated to one SyntheticEvent (for example, the doStuff in onTouchStart={doStuff} ).
  3. Returning every SyntheticEvent along with its dispatches.

The thing worth noting here is that no dispatches are actually executed in the plugin, as it only collects the functions themselves. (Most of the time, that is — some plugins do execute specific dispatches during the collection stage, but this is the exception rather than the norm). The SyntheticEvents can simply mirror native events (like click or drag ), or be more complex (like touchTap ), but are in all cases returned with their dispatch array attached, so as to be “ready for processing”.

To collect dispatches, React runs a double traversal of the component (be it native or DOM) tree, with a capture and bubbling phase that go from the root to the nested target (capture phase) back to the root (bubbling phase).

Double traversal

Note that for all differed dispatches (the ones executed outside the plugin itself, which is to say most of the dispatches), the double traversal happens in full. Interruptions such as stopPropagation() will take effect at dispatch time, effectively preventing the execution of subsequent functions for this SyntheticEvent only (see conclusion).

The EventPluginHub

All event plugins are injected into the EventPluginHub when the app launches, and plugins are sorted following a configuration file. Then, at runtime, EventPluginHub will perform the following each time it receives a native event:

  1. For each plugin (in order), gather all SyntheticEvents and their dispatch configuration and store them in the queue.
  2. Execute all dispatches for all events in the queue, effectively clearing it.

And that’s it! Your callbacks are executed with the right event. :)

Consequences & conclusion

An interesting consequence of this system is that a single native event can (and will, most of the time) generate multiple SyntheticEvents, each with a scope limited to the plugin that created it. This means that:

  • Only the nativeEvent part of the SyntheticEvent will be passed from plugin to plugin, so while modifications to nativeEvent can have repercussions over the executions of subsequent plugins, modifications to the SyntheticEvent cannot.
  • Due to the limited scope of theSyntheticEvent, calling methods such as stopPropagation() will only work for one event plugin.

As an example of that second point, let’s imagine that we have two plugins, A and B, defining, respectively, the synthetic events eventA and eventB. We will assume that these events have the following names: onEventA and onEventB for the bubbling phase, and onEventACapture and onEventBCapture for the capturing phase. Finally, both are triggered by the same top level type (say, topClick) and are ordered [A, B]. Now consider the following code in React Native (simply replace View with div for React web):

Any click event would first trigger a capturing phase for eventA , calling stopPropagation() in the nested component and effectively preventing the following bubbling phase. As expected, 'onEventA' will not appear. However, since eventB has been defined in a different plugin and therefore relies on a different SyntheticEvent , 'onEventB' will end up being printed to the console. Although this is arguably a pretty edge-case scenario, I can see times where this could cause unexpected behaviors.

There would of course be more to say about React’s event handling system, such as the fact that SyntheticEvents are actually pooled, but I have avoided these here in order to avoid overkill.

I sure learned a lot from traveling all around the code base (there is sadly not much in-depth documentation on this topic), and also from watching that great video by Kent C. Dodds, Dan Abramov & Ben Alpert. I hope that you might have learned a thing or two as well!

As for me, I’m going to continue having fun looking at how things work under the hood…

--

--

Hi! I am a software engineer at Playstation, Japan. I mostly talk about backend stuff, golang and vim. I also like cooking. My views are my own.