React 17 attaches events to the root DOM container instead of the document node


Before jumping into the problem statement, let’s understand, what is Event Delegation in Javascript for browsers, and how it is used in React.

Event Delegation

There are different types of events in Javascript. For example:-

  • Mouse Events: onClick, onMouseEnter, onDrag etc.
  • Form Events: onChange, onSubmit, etc.
  • Keyboard Events: onKeyUp, onKeyDown etc.
  • Window Events: onScroll etc.

When an event occurs on a page, the event flows from the document node to the target node and back to the document node.

There are four phases of the event.

  • (0) None: No event is being executed.
  • (1) Capture: Event handlers are executed down the tree (from document to target).
  • (2) At target: Event handlers are executed at the target node.
  • (3) Bubble: Event handlers are executed from target to parent.

Event Delegation in React 16 and earlier

React has been doing Event Delegation since the first release. When a DOM event fires on the document, respective event listeners get called.

In plain vanilla JS, an event listener is attached as below:

  let myButton = document.getElementById("myButton");
  myButton.addEventListener('click', (e) => {
    //Event handling logic.
  });

In React, we use the following code to add an event listener:

  <button onClick={(e) => {
    //Event handling logic.
   }}>
    My Button
  </button>

In React 16 or earlier, the event handlers are attached to the document node. React figures out which component to call when a DOM event gets fired by following Event bubbling for Event Delegation. Event bubbles up through component to document where native event handlers have been attached by React.

How event delegation helps?

Binding event listeners to each node is not feasible and affects the performance. For better performance, the event listeners are attached to the parent node (refer - jsperf test ). In the case of React 16 or earlier versions, the event listeners are attached to the document node.

Problem with Event Delegation on the document node

Consider an application, which has jQuery or vanilla JS embedded in React or has different React versions. If an event handler at the document node calls e.stopPropagation(), it causes a problem in React 16 and earlier versions. This behavior has resulted in some issues.

Let’s consider the use-case of the Terms and Conditions Page. The user has to click on the Accept button to accept the terms and conditions. If the user clicked on the ‘Accept’ button, he should get the option of rejecting it.

To render above page, here is smaple code of App.jsx.

import React, { useState } from "react";

export default function App() {
  const [buttonClicked, setButtonClicked] = useState(true);

  const handleClick = () => {
    setButtonClicked(!buttonClicked);
  }

  return (
    <div className="App">
      <h2>Event Delegation in React 16</h2>
      <div className="terms">
        Some Terms and conditions
      </div>

      <button
        className={buttonClicked ? "accept" : "reject"}
        onClick={handleClick}
      >
        {buttonClicked ? " Accept" : " Reject"}
      </button>
      <div className={buttonClicked ? "result-red" : "result-green"}>
        {buttonClicked
          ? "Please Accept Terms and conditons."
          : "Thanks for Accepting Terms and conditions"}
      </div>
    </div>
  );
}

The above code will work as per requirement. If someone adds the following piece of code at the index or parent node of React, it will start breaking the functionality.

const rootElement = document.getElementById("root");
rootElement.addEventListener("click", (e) => {
  e.stopPropagation();
});

Because of the e.stopPropagation(), the event handler written in React is not triggered.

Root cause of the problem

e.stopPropagation() stops bubbling up to parent nodes or stops capturing down to child nodes.

It is also important to understand the order of execution of the event handlers attached to a single node. In the majority of browsers, the event handlers execute in the order in which they are attached. In this case, the click event attached to the root will execute first. It prevents the event from bubbling to the document node, due to which the terms button click handler is not called.

Event Delegation in React 17

In React 17, React no longer attaches the event listeners at the document level. Instead, it attaches them to the root DOM container into which our React tree is rendered.

const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);

So the difference is React 17 calls rootNode.addEventListener() for most of the events, and the earlier versions, React calls document.addEventListener().

The above image is taken from React Official Document.

The event listener, which contains e.stopPropagation(), added to the index page will not affect the click event of terms page.

Check out the pull request and React Official Document to learn more.