Skip to main content

Event Propagation in React

· 8 min read
Hyunmo Ahn
Front End Engineer @ Line+

Introduction

When mixing React's event handler method with vanilla Javascript's event handler method, event propagation may not work as intended. For example, in a structure like button1, button2, clicking button2 can trigger the event handler of button1 as well.

const buttonEl = document.getElementById('button1');
buttonEl.addEventListener('click', () => {
console.log('button1 clicked');
});

const handleClick = (e) => {
e.stopPropagation();
console.log('button2 clicked');
};

return (
<button id='button1'>
<button id='button2' onClick={handleClick}>Click me</button>
</button>
);
// When button2 is clicked
button2 clicked
button1 clicked

Solution

Event propagation in React differs from that in vanilla Javascript because React handles event propagation using a delegation method. (comment)

In React (post React 17), event listeners are registered on the rootDOM. Therefore, event listeners within React propagate as expected according to the DOM structure, but vanilla Javascript event propagation may not behave as anticipated.

While it's best to avoid mixing the two types of events, sometimes it's unavoidable when using third-party libraries or handling parts of the code you can't control. In such cases, you must block event propagation according to the behavior of each method.

// Unify event listener to vanilla Javascript
const buttonEl = document.getElementById('button1');
buttonEl.addEventListener('click', () => {
console.log('button1 clicked');
});

const button2El = document.getElementById('button2');
button2El.addEventListener('click', (e) => {
e.stopPropagation();
console.log('button2 clicked');
});

return (
<button id='button1'>
<button id='button2'>Click me</button>
</button>
);

// Or
// Unify event listener to React
const handleClick1 = (e) => {
console.log('button1 clicked');
};

const handleClick = (e) => {
e.stopPropagation();
console.log('button2 clicked');
};

return (
<button id='button1' onClick={handleClick1}>
<button id='button2' onClick={handleClick}>Click me</button>
</button>
);

Step by Step

note

This explanation assumes you have an understanding of Javascript's Event Bubbling & Capturing.
If you're not familiar with it, refer to MDN.

To understand event propagation in React, let's look at the following example. Figure 1,2 show the UI when event propagation is set in each method. Try clicking anywhere on #1,2,3.

#1

#2

#3

Figure 1Vanilla Click Only

#1

#2

#3

Figure 2React Click Only

In Figure 1, only vanilla Javascript events are set, so the event propagates in red. Similarly, in Figure 2, only React events are set, so the event propagates in blue.

What happens if both types of events are set? Click Figure 3.

#1

#2

#3

Figure 3Vanilla & React Click

In Figure 3, both types of events are set, so both events occur, and the color is indicated in purple.

The order of event occurrence is as follows:

OrderTargetVanillaReactType
1#1Capture
2#2Capture
3#3Capture
4#3Bubble
5#2Bubble
6#1Bubble

Since event propagation wasn't blocked, all events occurred, and propagation took place.

Capturing occurs in the order of #1 -> #2 -> #3, and bubbling occurs in the order of #3 -> #2 -> #1.

What happens if we block event propagation? Click Figure 4,5.

note

The method used to block propagation is the same, using e.stopPropagation().

// React
<button onClick={(e) => e.stopPropagation()}></button>

// Vanilla
button.addEventListener('click', (e) => {
e.stopPropagation();
});

Figure 4 blocks React Event bubbling on #2, and Figure 5 blocks Vanilla Event bubbling on #2.

#1

#2

#3

Figure 4#2 Stop Propagation Bubbling by React

#1

#2

#3

Figure 5#2 Stop Propagation Bubbling by Vanilla

The behavior of the two examples is different. Let's summarize the results.

Figure 4. #2 Stop Bubbling by React

OrderTargetVanillaReactType
1#1Capture
2#2Capture
3#3Capture
4#3Bubble
5#2Bubble
6#1Bubble

Figure 5. #2 Stop Bubbling by Vanilla

OrderTargetVanillaReactType
1#1Capture
2#2Capture
3#3Capture
4#3Bubble
5#2Bubble
6#1Bubble

The results might seem complex, but let's focus on the event propagation method that was blocked.

In Figure 4, the React method was used to block propagation, so only the React method in Figure 4 did not trigger the event on #1. In Figure 5, the Vanilla Javascript method was used to block propagation, so only the Vanilla Javascript method in Figure 5 did not trigger the event on #1.

Up to this point, this is the event propagation method we are familiar with. Now, let's look at the results of the other method's event propagation.

In Figure 4, the Vanilla event occurred even though the React bubble propagation on #2 was blocked. Conversely, in Figure 5, the React event did not occur on #1, #2, or #3, even though the Vanilla bubble propagation on #2 was blocked.

The key point here is that React's event handler method doesn't directly register events on nodes but uses event delegation by registering them on the rootDOM (docs)

React event use event delegation
Figure 6React event use event delegation

Based on this fact, we can understand why the behavior of Figure 4 and Figure 5 differs.

In Figure 4, React event propagation was blocked, so the actual event propagation occurs at the rootDOM. This means the event propagation is actually implemented within React. Therefore, the actual event occurs in the following order:

  1. React event capturing at rootDOM
  2. Vanilla javascript event capturing at #1, #2, #3
  3. Vanilla javascript event bubbling at #1, #2, #3
  4. React event bubbling at rootDOM

Therefore, in Figure 4, the event propagation wasn't blocked during the 3rd step, the bubbling at #1, #2, #3, so all vanilla javascript events occurred.

So what about Figure 5? Since vanilla javascript's event was blocked at #2, the event propagation was interrupted on #2 during the 3rd step, and no events occurred at #3 or during the 4th step, at the rootDOM. Therefore, all React bubbling events in Figure 5 were interrupted.

What about blocking Capturing in another example? Let's look at Figure 7,8. It might be interesting to predict the behavior before running it. Both examples blocked event capturing at #2.

#1

#2

#3

Figure 7#2 Stop Propagation Capturing by React

#1

#2

#3

Figure 8#2 Stop Propagation Capturing by Vanilla

The results are as follows.

Figure 7. #2 Stop Capturing by React

OrderTargetVanillaReactType
1#1Capture
2#2Capture
3#3Capture
4#3Bubble
5#2Bubble
6#1Bubble

Figure 8. #2 Stop Capturing by Vanilla

OrderTargetVanillaReactType
1#1Capture
2#2Capture
3#3Capture
4#3Bubble
5#2Bubble
6#1Bubble

Did you get the answer right?

In Figure 7, React capturing was blocked, which occurs at the rootDOM. Therefore, React capturing occurred at #1, #2. However, vanilla javascript capturing was interrupted at the rootDOM, so no events occurred.

In Figure 8, vanilla javascript capturing was blocked, which occurs at #2. Therefore, vanilla javascript capturing occurred at #1, #2. However, React capturing occurs at the rootDOM, so even though the event was blocked at #2, all capturing events occurred.

Conclusion

React registers events on the rootDOM and handles event propagation internally, so it behaves differently from vanilla javascript event propagation. Generally, there's no need to know this in detail, but when mixing React and vanilla javascript events, issues can arise in event propagation.

This applies not only to code managed within the project but also when using third-party libraries, so unexpected issues can occur. The author encountered this issue when using the swiper library in a React environment, leading to this discovery during root cause analysis.

Therefore, it's recommended to be aware of React's event registration method when using it.

Reference