Events in React and a Gotcha

Interactivity is a key aspect of modern frontend applications, it is enabled by HTML events and Javascript event handlers. Any action performed on a web page is processed because an event was fired for the action.

It is very interesting to learn how these events work, their lifecycle and how they can be handled at multiple levels. There are also fascinating differences on how ReactJs implements events.

In this article, we will mainly focus on the lifecycle and journey of events and compare between HTML events and React events.

Event propagation

HTML events not only fire on the element but also on all its parents in the DOM hierarchy and this is called propagation. This enables a powerful feature called event delegation which lets us handle events generated on elements by their parent elements.

In all modern browsers propagation happens in two ways, capturing and bubbling. To explain these both, let’s consider the following DOM structure with divs and paragraph and also consider user clicked on the paragraph element.

<div1>
  <div2>
    <p>Text</p>
  </div2>
</div1>

Capturing

The event first fires on the root node and then propagates down the tree until it reaches the target (element on which the action was performed). In our example, it fires on div1 first, then on div2 and finally on paragraph(target).

div1 -> div2 -> p

Bubbling

The event first fires on the target and then propagates up the tree until it reaches the root node. This is the default mechanism with with handler work. Below is how it works with our example.

p -> div2 -> div1

In real world both of these occur together, it starts capturing first, reaches the target and the bubbles back up the tree.

                  TARGET
                    |
 --CAPTURING--      |      --BUBBLING---
{             }     |     {             }      
                    V        
div1  ->  div2  ->  p  ->  div2  ->  div3

How react handles events

React uses synthetic events which follows the same interface as DOM events while fixing any inconsistencies in browser implementations. There are some differences from the DOM events in the interface and the way we use them, you can read about it in the documentation.

React adds a single event handler per event type on the root element and then triggers the corresponding synthetic event which propagates the same way as a DOM event does, but it happens on the virtual DOM. So as a developer we can be rest assured that the bubbling and capturing works the same it would with native DOM elements.

The gotcha

Consider the following react code, here we have a containing div and a button inside it. We have added a click handler to both the container and the button and appropriate console log statements in each handler.

export default function App() {
  const handleContainerClick = () => {
    console.log("Container clicked");
  };
  useEffect(() => {
    document
      .getElementById("container")
      .addEventListener("click", handleContainerClick);

    return () => {
      document
        .getElementById("container")
        .removeEventListener("click", handleContainerClick);
    };
  }, []);

  return (
    <div className="App">
      <h1>pixawise</h1>
      <div id="container">
        <p>
          This container has the click handler attached using{" "}
          <code>addEventListener</code>
        </p>
        <p>Below button has click handled attached with react prop</p>
        <button
          onClick={() => {
            console.log("Button clicked");
          }}
        >
          Click Me
        </button>
      </div>
    </div>
  );
}

When the user clicks on the button, what is the expected sequence in which the console statements would be printed to console?

Did you say

Button clicked
Container clicked

Even I expected that, But, You will get it in reverse order.

Container clicked
Button clicked

Try it your self in code sandbox.

Demonstrated preview of actual behavior of the above code
Events gotcha in React

Explanation

There is one difference in the way we attached event handlers to container and button. For container, we used addEventListener which is a DOM api and attaches the event handler to real DOM element. But for button we used react props, so its a synthetic event and only works on the virtual DOM.

React synthetic events trigger after the events are propagation of DOM elements is complete. Because the event has to first propagate to the root element on which react handles all events and fires react events to virtual DOM. Only after that propagation begins in virtual DOM.

So even if you may have used stopPropagation on buttons handler, it would have still triggered the handler on container. So, the take away is to be careful while attaching DOM event handlers in React code. I would say use React prop syntax for attaching handlers as much as possible and only fallback to addEventListener if it is absolutely necessary and even the be mindful of these gotchas.

Thats all for this article. Happy coding!!

Thank you for reading thus far. If you find this article helpful and think any one you know may get benefitted by this, please feel free to share it.

--