Everything about event bubbling/capturing
Definition
What is event bubbling?
Most events attached to the DOM node do propagate (bubble/capture). In other words, when an event is triggered
- It first goes from the most parent event (i.e., the
window
object) to the lowest descendant. This phase is called capturing.
Note: IE does not support event capture. - After that, the event goes back from the lowest descendant to the highest ascendant (the
window
object). This phase is called bubbling.
Event capture with react
Due to the inconsistency across browsers regarding the support for event capture in particular and event support in general, react encapsulates the native events in the React's Event System called SyntheticEvent.
The default on... props are for the bubbling phase, to listen for the events in the capture phase, suffix the prop names with Capture
. For example: onClickCapture
, onChangeCapture
, ...
From react docs
The event handlers below are triggered by an event in the bubbling phase. To register an event handler for the capture phase, appendCapture
to the event name; for example, instead of usingonClick
, you would useonClickCapture
to handle the click event in the capture phase.
React supports an extensive range of events, namely: copy
, cut
, paste
, compositionend
, compositionstart
, compositionupdate
, keydown
, keypress
, keyup
, blur
, focus
, change
, input
, invalid
, reset
, submit
, error
, load
, select
, touchcancel
, touchend
, touchemove
, touchstart
, scroll
, wheel
, animationstart
, animationend
, animationiteration
, transitionend
, toggle
, media events, pointer events, mouse events.
Document type
Note: I said window
is the highest ascendant object in the event bubbling/capture tree. One may question what the nearest child element of the window object is.
The following facts can solve this question:
- The equivalent javascript DOMElement of the
<html>
tag isdocument.documentElement
. document
is the parent of the<html>
element.window
is a parent of thedocument
object.
// document.documentElement is the top DOM element being displayed, not document
document.documentElement === document.getElementsByTagName('html')[0] // true
// document is type Document, <html> is type Element
document.documentElement instanceof Element // true
document.documentElement instanceof Document // false
document instanceof Element // false
document instanceof Document // true
// they both are Node type, because Element and Document inherit Node
document instanceof Node // true
document.documentElement instanceof Node // true
// window is not Node type
window instanceof Node // false
window instanceof Window // true
// Document and Element have the different implementations
document.getElementsByClassName === document.documentElement.getElementsByClassName // false
document.body.getElementsByClassName === document.documentElement.getElementsByClassName // true
// Several methods do not exist in the Element's prototype
document.body.getElementById === undefined // true
typeof document.getElementById === 'function' // true
Hence, some top objects in the event tree are window
-> document
-> document.documentElement
(<html>
element) -> ...
Which events do not bubble?
Most events do bubble. There are several exception events that do not bubble. Some of them are:
- focus (focusin is the equivalent bubbling version).
- blur (focusout is the equivalent bubbling version).
- load, unload, abort, error, beforeunload.
- mouseenter (mouseover is similar but do bubble).
- mouseleave (mouseout is similar but do bubble).
- DOMNodeInsertedIntoDocument, DOMNodeRemovedFromDocument (both events are deprecated).
- In IE prior to version 9: change, submit and reset.
- and many others...
This information can be checked from MDN Event reference or W3C UI Events.
The capability of bubbling/capturing is defined when constructing the event via the bubbles: boolean
option.
Are non-bubble events captured?
The answer is yes, unless the browser does not support event capture. See this codepen to confirm.
Event listener firing order
Phase specification
EventTarget.addEventListener() is used to add a handler to an event. The useCapture
option is used to specify whether the handler is added to the capture phase or the bubble phase.
Most today browsers do not require the useCapture
option and use the default false
value. However, as noted by MDN docs, there are some cases that require this option, such as IE prior to version 9. Thus, to improve your code compatibility, you should always explicitly set this option.
Note: useCapture
has not always been optional. Ideally, you should include it for the widest possible browser compatibility.
Firing order
From the illustration, it is trivial to infer that event listeners in the capture phase are fired before the bubble phase.
In the target phase, where the event handlers attached to the event target are fired, the useCapture
parameter is ignored and the event handlers are executed in the order they were registered.
From MND docs
Note: For event listeners attached to the event target, the event is in the target phase, rather than the capturing and bubbling phases. Events in the target phase will trigger all listeners on an element in the order they were registered, regardless of the useCapture
parameter.
Canceling an event
There are three types of event canceling.
- preventDefault():
- has no effect on non-cancelable events.
- prevents the default handler of the event, such as form submission, text input, ... - stopPropagation(): prevents the event to continue the capture/bubble chain.
- stopImmediatePropagation(): prevents the event listeners attached to the same target from being fired.
All three calls have different event listeners which are affected. None of them affects the target listeners of any other.
Event target
The are several target-related properties in the event object which are handy to obtain useful context information because sometimes the element that triggers the event is not always the element where the event listener is attached to. Some of these properties are:
- Event.target: the element that triggers the event.
- Event.currentTarget: the element where the event listener is attached to.
Note: In vanilla javascript, and React prior to version 17 (see Event Pooling), the event object is re-used in the capture/bubble itinerary. See this pen to confirm this fact in a vanilla script, or this pen for React 17 version.
This means that if the event listener is asynchronous, you should cache the event's properties locally, otherwise they may be changed unexpectedly.
const clickHandler = evt => {
const {currentTarget} = evt // cache property locally
setTimeout(() => {
console.log('evt.currentTarget changed', evt.currentTarget !== currentTarget)
}, 3000)
}
For mousenter, mouseleave, mouseout, mouseover, dragenter, dragexit, blur, focus, focusin, focusout events, the event has one more property named relatedTarget
which points to the secondary target, where target
is the primary target. Use the following structure for your convenience to infer them.
Event do <action> the <primary target> from/to the <secondary target>
For example:
- mouseenter: mouse enters/overs/dragenter event.target from event.relatedTarget
- mouseleave: mouse leaves/outs/dragexit event.target to event.relatedTarget
Focus/Blur events
relatedTarget property
There are 4 types of focus/blur events: blur/focus (non-bubble), focusin/focusout (bubble).
There was a bug from many years ago in Firefox (it has been fixed along), where relatedTarget
is null on all these 4 events.
Even though the W3C specs on blur, focus, focusin, focusout does require relatedTarget
.
FocusEvent.relatedTarget : event target receiving focus.
FocusEvent.relatedTarget : event target losing focus (if any).
IE only assigns the relatedTarget
on focusin/focusout events, while it does not set on focus/blur events (source).
React's onBlur/onFocus
Prior to version 17, React checks for the availability of event capturing to use the blur/focus events; If not, use the focusin/focusout event. This causes several problems.
From version 17, they decided to use focusin/focusout under the hood for onFocus/onBlur.
Anyway, regardless of react version, react's onBlur/onFocus is not native focus/blur events, because in react onBlur/onBlur do bubble while the native focus/blur events do not bubble.
Check if an element loses/gains its focus in react
Because the onFocus/onBlur events bubble in react, if a child element gains/loses from from/to its sibling element.
To correctly determine if the target element (where the event listener is attached to) loses/gains its focus, check for evt.currentTarget === evt.target
.
The following source code is copied from here.
export default function App() {
return (
<div
tabIndex={1}
onFocus={(e) => {
console.log("focusin (self or child)");
if (e.currentTarget === e.target) {
console.log("focus (self)");
}
if (!e.currentTarget.contains(e.relatedTarget)) {
console.log("focusenter");
}
}}
onBlur={(e) => {
console.log("focusout (self or child)");
if (e.currentTarget === e.target) {
console.log("blur (self)");
}
if (!e.currentTarget.contains(e.relatedTarget)) {
console.log("focusleave");
}
}}
>
<input />
<input />
</div>
);
}
How much does .addEventListener / .removeEventListener affects the performance?
I added this session because I saw many people care about tuning their code to save several calls of .addEventListener()
in the effect callback.
My suggestion is that: please do NOT do that. Instead, save your time and spend the investigation on the other critical portion of your program. .addEventListener
/.removeEventListener
are very cheap in terms of performance.
The following snippets perform relatively the same. Precisely speaking, the .addEventListener
versions are at least faster or equal to the later versions.
function App(){
const ref = useRef()
const cb = () => console.log('element is clicked')
useEffect(() => {
ref.current.addEventListener('click', cb, false)
return () => void ref.current.removeEventListener('click', cb)
}) // run in every render
return <button type="button" ref={ref}>Click me!</button>
}
function App(){
const ref = useRef()
const cb = () => console.log('element is clicked')
useEffect(() => {
ref.current.addEventListener('click', cb, false)
return () => void ref.current.removeEventListener('click', cb)
}, [cb]) // run in every render because cb always changes
return <button type="button" ref={ref}>Click me!</button>
}
function App(){
const cb = () => console.log('element is clicked')
return <button type="button" onClick={cb}>Click me!</button>
}
function App(){
return <button type="button" onClick={() => console.log('element is clicked')}>Click me!</button>
}