Examining React's Synthetic Event: the nativeEvent, the eventPhase, and Bubbling.

Perhaps you've just started working with React, and you're working with event handlers and you've noticed that when you get the Event Object back, it doesn't look quite the same as it did in vanilla JS.

What you're getting back instead is the SyntheticEvent (SE) and it contains the original event object in what React has dubbed the nativeEvent (nE).

What is the SyntheticEvent?

Straight from the React Docs, it is "a cross-browser wrapper around the browser’s native event ... except the events work identically across all browsers."

To examine this, I've built a basic React component with an onClick button.

function ButtonDemo(){



 function showEventDifferences(e) {
      console.log(e)
      console.log(e.nativeEvent)
   }


return (

<div>

 
  <button 
  onClick={showEventDifferences}
  className="lookingAtClick">
            Discover Events
    </button>

</div>
)

}

This logs the SE first, and the nE second when clicking the discover events button. If you were to click on button within the demo component, you'd get something like this back:

SyntheticBaseEvent 
{_reactName: "onClick", 
_targetInst: null, 
type: "click", 
nativeEvent: MouseEvent, 
target: button.lookingAtClick, …}
altKey: false

bubbles: true

button: 0

buttons: 0

cancelable: true

clientX: 259

clientY: 618

ctrlKey: false

currentTarget: null

defaultPrevented: false

detail: 1

eventPhase: 3

getModifierState: ƒ modifierStateGetter(keyArg)

isDefaultPrevented: ƒ functionThatReturnsFalse()

isPropagationStopped: ƒ functionThatReturnsFalse()

isTrusted: true

metaKey: false

movementX: 0

movementY: 0

nativeEvent: MouseEvent {isTrusted: true, 
screenX: 1723, screenY: 752, 
clientX: 259, clientY: 618, …}

pageX: 259

pageY: 618

relatedTarget: null

screenX: 1723

screenY: 752

shiftKey: false

target: button.lookingAtClick

timeStamp: 734167.6999999881

type: "click"
view: Window {window: Window, self: Window, 
document: document, name: "", location: Location, …}

_reactName: "onClick"

_targetInst: null

__proto__: Object



MouseEvent {isTrusted: true, 
screenX: 1723, screenY: 752, 
clientX: 259, clientY: 618, …}
altKey: false
bubbles: true

button: 0

buttons: 0

cancelBubble: false

cancelable: true

clientX: 259

clientY: 618

composed: true

ctrlKey: false

currentTarget: null

defaultPrevented: false

detail: 1

eventPhase: 0

fromElement: null

isTrusted: true

layerX: 259

layerY: 618

metaKey: false

movementX: 0

movementY: 0

offsetX: 90

offsetY: 13

pageX: 259

pageY: 618
path: (8) [button.lookingAtClick, div,
 div, div#root, body, html, document, Window]
relatedTarget: null

returnValue: true

screenX: 1723

screenY: 752

shiftKey: false
sourceCapabilities: InputDeviceCapabilities 
{firesTouchEvents: false}

srcElement: button.lookingAtClick
target: button.lookingAtClick

timeStamp: 734167.6999999881

toElement: button.lookingAtClick

type: "click"
view: Window {window: Window, 
self: Window, document: document, 
name: "", location: Location, …}

which: 1

x: 259

y: 618

__proto__: MouseEvent

Let's filter that to make it a little more readable. What the SyntheticEvent provides that's different:

SyntheticBaseEvent:
{_reactName: "onClick", 
_targetInst: null, type: "click", 
nativeEvent: MouseEvent, target: button.lookingAtClick, …}
...
eventPhase: 3

getModifierState: ƒ modifierStateGetter(keyArg)

isDefaultPrevented: ƒ functionThatReturnsFalse()

isPropagationStopped: ƒ functionThatReturnsFalse()

nativeEvent: MouseEvent {isTrusted: 
true, screenX: 1723, screenY: 752, 
clientX: 259, clientY: 618, …}

_reactName: "onClick"

_targetInst: null

__proto__: Object

The mouse event:

MouseEvent {isTrusted: true,
 screenX: 1723, screenY: 752, 
clientX: 259, clientY: 618, …}
cancelBubble: false

composed: true

eventPhase: 0
currentTarget: null

layerX: 259

layerY: 618

offsetX: 90

offsetY: 13

returnValue: true

sourceCapabilities: InputDeviceCapabilities 
{firesTouchEvents: false}

srcElement: button.lookingAtClick

which: 1

x: 259

y: 618

__proto__: MouseEvent

And their overlap:

altKey: false

bubbles: true

button: 0

buttons: 0

cancelable: true

clientX: 259

clientY: 618

ctrlKey: false

defaultPrevented: false
isTrusted: true
metaKey: false

movementX: 0

movementY: 0

pageX: 259

pageY: 618

relatedTarget: null

screenX: 1723

screenY: 752

shiftKey: false

target: button.lookingAtClick

timeStamp: 734167.6999999881

type: "click"

view: Window {window: 
Window, self: Window, document: 
document, name: "", location: Location, …}

When looking at this, what's perhaps surprising is how much this wrapper SE and its nE child have in common. React's SE has bundled at the top level of the SE most of what a dev would need for the majority event handling. This should make the need to drill into the nE relatively uncommon. Except when you need to do something obvious, such as needing to access the MouseEvent 'which' key value. Enjoy trying to Google what that does.

Pulling Back the Curtain

However, some of the differences are also striking. A small one is that you can see a little bit of the abstraction that is taking place under the hood of React with its synthetic wrapper element around the MouseEvent. This was what React is talking about when it discusses how it works across all browsers. Note the __reactName: "onClick", which points to the fact that somewhere in the compilation process (as Javascript is passed through Babel to become JS code that can be read by the browser), there is code that, in rough pseudocode amounts to:

React.createEvent("OnClick", () => {
 if (browser === Safari){
  {return new React.SyntheticObject(Safari)
}
  else if (browser === Chrome){ 
  {return new React.SyntheticObject(Chrome)}
}
  else if ...

This way of having React handle the heavy lifting is in stark contrast to, for example, working with vanilla CSS, in which one can spend a fair amount of time and additional space writing repetitive code that ensures that the various browsers will display similar looking experiences by adding -webkit-, -moz-, or various other prefixes to ensure compatibility.

Drilling Down to the Root

Beyond giving us a peek at the abstraction, there's something else interesting in this object. Take a look at the proto key. The SE comes with a different class constructor than does the nE! While it is called an Object, this is not the plain old JavaScript object (we'll see that soon enough). Instead, this is where you'll find the .preventDefault(), .stopPropogation() and the now defunct (as of React 17) .persist() method which helped with asynchronous JS due to React previously using a pooling process for its events. When you call any of these methods as part of an event handler function, the reason they work is because they are instanced as part of the SE object itself.

The relatively small SE prototype though is put to shame by the much more massive nE which has a laundry list of getter functions which allow it to create the various components such as the pagex/y locations of the click, if any buttons are being held at the time of the click, and at what time the event happened (among many others). It also shows that the MouseEvent object is not the end of the line as far as the nativeElement constructor. The MouseEvent object itself is an extension of the UIEvent class:

...
__proto__: MouseEvent
(...)
get x: ƒ x()
get y: ƒ y()
__proto__: UIEvent
  bubbles: (...)
  cancelBubble: (...)
  cancelable: (...)
  composed: (...)
  (...)

Which, in turn, is an extension of the Event class ...

...
__proto__: UIEvent
(...)
get which: ƒ which()
__proto__: Event
   AT_TARGET: 2
   BUBBLING_PHASE: 3
   CAPTURING_PHASE: 1
   (...)

And then finally finds its most basic root class which is a plain old JS object.

___proto__: Event
(...)
get timeStamp: ƒ timeStamp()
get type: ƒ type()
__proto__:
   constructor: ƒ Object()
   hasOwnProperty: ƒ hasOwnProperty()
   isPrototypeOf: ƒ isPrototypeOf()
   (...)

Told you we'd get here. So why did we trek down this particular rabbit hole? The point is that React's abstraction can be something of a double edged sword. By adding an additional layer of polish which helps us to write code more quickly and cleanly, it can sometimes make it harder to understand what is actually happening.

event.eventPhase and Bubbling

This brings me to the final example, the event.eventPhase attribute. For greater detail on the .eventPhase, feel free to parse its MDN page, but to keep it short - here it is as follows:

eventPhase = 0 : No event exists.
eventPhase = 1 : The event is being capture. To see this phase, instead of calling onClick in React, make use of the onClickCapture, or add 'Capture' to almost all of the 'onAction' events (ie OnChangeCapture).
eventPhase = 2 : The event has arrived in the code/function and is ready to be used. If there is no *bubbling the eventPhase should terminate here.
eventPhase = 3 : If there is bubbling, the event terminates after this point.

Bubbling refers to the fact that when an event is triggered at the local/initial level, it will then proceed to the parent level to look for additional events and if it finds any it will set those events in motion, and then it will continue through connected ancestor elements until all events have been triggered. This 'upward' movement through parent elements in the DOM structure can help you visualize the term as it 'bubbles up'.

So why does the React onClick element return an eventPhase of 3 when there is nothing else on the page that we have rendered? What is causing the bubbling? If we make a code snippet for vanilla JS that mimics our previous React element, like so:

in index.html:

<body>
      <button class="lookingAtClick">Discover Events</button>
   <script src="./index.js"></script>
 </body>

in index.js :

function testEventPhase(e) {
   console.log(e.eventPhase)
   console.log(e)
}

document.querySelector(".lookingAtClick").addEventListener("click", testEventPhase)

Why do we get back an eventPhase of 2 on our click? Why would a vanilla JS version eventPhase terminate earlier than the React eventPhase?

The answer, which we can probably guess from our eventPhase chart is because bubbling is happening. What might not be clear though is that's because React events always bubble. This isn't new for a click event, but it is different for some other common JavaScript events such as 'focus', 'blur', and 'change' which do not have this behavior in JS. This bubbling won't be an issue for our simple console logging button functional component, but not realizing that all events in React trigger other nested events can lead to rapid-onset baldness when trying to debug your code.

Just remember if this starts happening to you - there is a reason why the aforementioned .stopPropagation() is instanced on the SE class to begin with.

In short, frameworks and libraries can make our lives easier, but they can also make them more confusing if we don't realize that the glossy sheen also has an additional layer of rules, extensions, and interactions on top of our base language. Discovering these nuances and realizing how to troubleshoot the new problems are just part of the process of figuring it all out!

21