Why isn't event.stopPropagation() working?!
Tags: react, webdev, javascript, canopy
Published: 2020-07-09 00:48:53

Today I spent a number of hours with a coworker trying to figure out why a dropdown component was unmounting on certain clicks. 

There is a cool date-range picker built by another coworker that allows the user to select two different days like so:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/rt0ciwy8ef2l4qmtcmmp.gif)

Our QA had discovered a bug where clicking a day in June when looking at May caused the entire thing to close.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/q3495h250aqk1reg7698.gif)

What? Why is that happening?

We had solved a similar problem a few months ago so we spent a minute tracking slack and git history to try and see what we had changed. Eventually my coworker discovered the problem: there was an event that was happening on click of document in which we were checking to see if the event's target was a child node and closing if not the case.

```jsx
componentDidMount() {
  document.addEventListener('click', this.handlePageClick);
}

handlePageClick = e => {
  if (!this.myDiv.contains(e.target)) {
    this.hideChild();
  }
};
```

But myDiv does contain the target..? Well yes, in every case except the May example. You'll notice in the May example there is an extra row. That row disappears as soon as you click a day in June because our component is unmounting those nodes before the document click event listener is running. Watch what happens when I disable the click event.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/tkv8ina70d37f4qt86gw.gif)

If the document click event listener was active, it'd try and find the DOM node that is no longer there on that 6th row.

There are a few ways we could have approached this.
* have a ghost 6th row that is always present but only sometimes visible
* delay the removing of the row until the document click event fire finished (callback, timeout?)
* add a property to the event.nativeEvent object that we could check for from the document event listener to conditionally unmount

We eventually decided to pursue event bubbling. It didn't make sense to have any click events bubbling out of the popup, so why not kill them? Problem is: they already were killed. We saw this all over the code:

```jsx
<div onClick={e => e.stopPropagation()}>
```

So why then was it still firing? Why isn't `event.stopPropagation()` working?!

I came to the conclusion that the document event listener was indifferent to any children clicks after drafting a [codesandbox](https://codesandbox.io/s/kind-hoover-vvfl2?file=/src/App.js) that showed the document event listener firing regardless of stopPropagation.

But then my coworker tweeked [w3school's sandbox](https://www.w3schools.com/JSREF/tryit.asp?filename=tryjsref_event_stoppropagation) to show me I was wrong. So I was then scratching my head.

Then it hit me. [React's SyntheticEvent](https://reactjs.org/docs/events.html). I remembered that React uses a single event listener on the document. If we're adding others and trying to stop propagation, that's probably meaningless to other document event listeners because they're on the same node.

I learned that the workaround is to use [window.addEventListener()](https://stackoverflow.com/a/52879137) instead which solved our problem instantly. Too many hours to learn a hard lesson: review and master the React docs.

```jsx
componentDidMount() {
  // document.addEventListener('click', this.handlePageClick);
  window.addEventListener('click', this.handlePageClick);
}
```