Search code examples
javascriptreactjsaddeventlistenerref

addEventListener on refs in react showing strange behaviour


I have created 2 components in a parent child relation. The first component is using refs and in componentDidMount I added a click event listener on the DOM node, in the child component also I added a onclick listener but this time using the props on the DOM element.

But now when I click the child component the event listener of the parent component is getting invoked, and if I add the onClick on parent component using the props instead of refs everything works fine.

So, can anybody tell me why this behaviour is shown when we add event listener using the refs.

class SecondDiv extends React.Component {
  
  divClicked = (event) => {
    event.stopPropagation();
    console.log('Second Div clicked');
  }
  
  render() {
    return <div className="Div2" onClick={this.divClicked} >
      This is second div
    </div>
  }
}

class MovieItem extends React.Component {

   constructor(props) {
     super(props);
     this.node = React.createRef();
   }
  
  componentDidMount() {
    this.node.current.addEventListener('click', this.divClicked);
  }

  componentWillUnmount() {
    this.node.current.removeEventListener('click', this.divClicked);
  }
  
  divClicked = (event) => {
    event.stopPropagation();
    console.log('First div clicked');
  }

  render() {
    // 1st approach
    return <div ref={this.node} className="Div1" >
      <p>This is Div 1</p>
      <SecondDiv />
    </div>
    
    // 2nd approach
    // return <div ref={this.node} className="Div1" onClick={this.divClicked} >
    //   <p>This is Div 1</p>
    //   <SecondDiv />
    // </div>
  }

}

ReactDOM.render(<MovieItem />, document.getElementById("app"));
<div id="app"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Also on CodePen


Solution

  • It's because React uses event delegation to provide synthetic event handlers, you're using stopPropagation on your DOM event handler. Since the handlers it uses are on the root element (id="app" in your case), if you use a DOM handler to stop the event from propagating, React doesn't see it and doesn't trigger your event handlers.

    Your elements are like this:

    +−−−−−−−−−−−−−−−−−−−−−−+
    |         #app         |
    |  +−−−−−−−−−−−−−−−−−+ |
    |  |     .Div1       | |
    |  |  +−−−−−−−−−−−+  | |
    |  |  |  .Div2    |  | |
    |  |  +−−−−−−−−−−−+  | |
    |  +−−−−−−−−−−−−−−−−−+ |
    +−−−−−−−−−−−−−−−−−−−−−−+

    React's DOM handler is on #app. Your DOM handler is on .Div1. When you click .Div2, it bubbles to .Div1 where your DOM handler sees it and stops it, so it never reaches #app. Since it doesn't reach #appp, React doesn't do its synthetic event for .Div2.

    In general, don't use DOM event handlers when you can use React event handlers instead. But if you need to use a DOM handler, and you want React's handlers to also run, don't prevent propagation.

    Here's that example with stopPropagation commented out in the DOM handler:

    class SecondDiv extends React.Component {
      
      divClicked = (event) => {
        event.stopPropagation();
        console.log('Second Div clicked');
      }
      
      render() {
        return <div className="Div2" onClick={this.divClicked} >
          This is second div
        </div>
      }
    }
    
    class MovieItem extends React.Component {
    
       constructor(props) {
         super(props);
         this.node = React.createRef();
       }
      
      componentDidMount() {
        this.node.current.addEventListener('click', this.divClicked);
      }
    
      componentWillUnmount() {
        this.node.current.removeEventListener('click', this.divClicked);
      }
      
      divClicked = (event) => {
        // event.stopPropagation(); // <============== Removed
        console.log('First div clicked');
      }
    
      render() {
        // 1st approach
        return <div ref={this.node} className="Div1" >
          <p>This is Div 1</p>
          <SecondDiv />
        </div>
        
        // 2nd approach
        // return <div ref={this.node} className="Div1" onClick={this.divClicked} >
        //   <p>This is Div 1</p>
        //   <SecondDiv />
        // </div>
      }
    
    }
    
    ReactDOM.render(<MovieItem />, document.getElementById("app"));
    <div id="app"></div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

    As you can see, it sees both events now, but as you noted in a comment, you see them in an odd order:

    First div clicked
    Second Div clicked

    That's, again, because React's click handler is on #app, so it doesn't fire its synthetic event for .Div2 until the DOM event for .Div1 has already been fired.