React—add a click listener to dangerouslySetInnerHTML sub-element

Viewed 1690

I have this html text:

"<span id='capo' class='capo' data-capo=3>Capo 3</span>"

This is part of a larger html that is inserted via dangerouslySetInnerHTML

And I have a listener like this:

componentDidMount() {
  document.getElementById('capo').addEventListener('click', this.changeKey);
}

However this only works for one click—after that the listener doesn't exist. I'm guessing because React has replaced the component (since I change the state in this.changeKey), and therefore the listener no longer exists.

How do I reliably set a listener for React components?

2 Answers

In this case you can make use of event bubbling!

Just add your event listener to the upper element (the one with dangerouslySetInnerHTML will be fine) listen for clicks and make sure that e.target.id is equal to the desired element ID.

Check out the example below (it uses dangerouslySetInnerHTML just to show how event bubbling works and how to use it with React, you shouldn't write component like this in the real life):

class Element extends React.Component {
  state = {
    loadTime: 0,
    lastUpdate: 0
  };
  
  lastUpdateTimer = null;

  componentDidMount() {
    this.resetTimer();
    
    this.lastUpdateTimer = setInterval(this.setLastUpdate, 1000);
    
    this.dynamicContentElement.addEventListener("click", this.handleClick)
  }
  
  componentWillUnmount() {
    clearInterval(this.lastUpdateTimer);
    this.dynamicContentElement.removeEventListener(this.handleClick)
  }
  
  resetTimer() {
    const now = Date.now();

    this.setState({
      loadTime: now,
      lastUpdate: now
    });
  }
  
  setLastUpdate = () => {
    this.setState({
      lastUpdate: Date.now()
    });
  }

  getDynamicContent() {
    const time = Math.round((this.state.lastUpdate - this.state.loadTime) / 1000);
    return {
      __html: `<p>
        You are here: <strong>${time}</strong> seconods.
        <br>
        <button id="btn">Click to reset counter</button>
       </p>`
    };
  }
  
  handleClick = (e) => {
    if (e.target.id === "btn") {
      this.resetTimer();
    }
  }

  render() {
    return (
      <div>
        Dynamic content below:
        <div
          ref={el => this.dynamicContentElement = el}
          dangerouslySetInnerHTML={this.getDynamicContent()}
        />
      </div>
    );
  }
}


ReactDOM.render(
  <Element />,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

Whenever there is a state change, React removes the html and replaces it.

This code will only execute when the component is first loaded, not when state changes and html elements (and their listeners) are wiped:

componentDidMount() {
  document.getElementById('capo').addEventListener('click', this.changeKey);
}

But this code will run every time the component is rerendered (so whenever a new html element is made, a new listener is attached to it):

componentDidUpdate() {
  document.getElementById('capo').addEventListener('click', this.changeKey);
}
Related