21
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).
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.
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.
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.
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