Caveat: This sounds like an X/Y problem, where the underlying problem (whatever it is) should be solved differently, so that you don't have to add a click handler to a DOM element created via dangerouslySetInnerHTML
(ideally, so you don't have to create DOM elements via dangerouslySetInnerHTML
at all). But answering the question you asked: (You've clarified the use case; solution #1 below applies and isn't poor practice.)
I don't think you can do that directly. Two solutions I can think of:
Use delegated event handler on the div
: Add a click handler on the div
, but then only take action if the click passed through the b
element.
Use a ref
on the div
, and then hook the click handler up in componentDidMount
and componentDidUpdate
(finding the b
element within the div
via querySelector
or similar), something along these lines:
Here's an example of #1:
<div onClick={this.clickHandler} dangerouslySetInnerHTML={this.createMarkup(string)}/>
...where clickHandler
is
clickHandler(e) {
// `target` is the element the click was on (the div we hooked or an element
// with in it), `currentTarget` is the div we hooked the event on
const el = e.target.closest("B");
if (el && e.currentTarget.contains(el)) {
// ...do your state change...
}
}
...or if you need to support older browsers without ParentNode#closest
:
clickHandler(e) {
// `target` is the element the click was on (the div we hooked or an element
// with in it), `currentTarget` is the div we hooked the event on
let el = e.target;
while (el && el !== e.currentTarget && el.tagName !== "B") {
el = el.parentNode;
}
if (el && el.tagName === "B") {
// ...do your state change...
}
}
...and where you bind clickHandler
in the constructor (rather than using a property with an arrow function; why: 1, 2):
this.clickHandler = this.clickHandler.bind(this);
Live Example:
let string = "Hello <b>Click here</b>";
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
clicks: 0
};
this.clickHandler = this.clickHandler.bind(this);
}
clickHandler(e) {
// `target` is the element the click was on (the div we hooked or an element
// with in it), `currentTarget` is the div we hooked the event on
// Version supporting older browsers:
let el = e.target;
while (el && el !== e.currentTarget && el.tagName !== "B") {
el = el.parentNode;
}
if (el && el.tagName === "B") {
this.setState(({clicks}) => ({clicks: clicks + 1}));
}
// Alternative for modern browsers:
/*
const el = e.target.closest("B");
if (el && e.currentTarget.contains(el)) {
this.setState(({clicks}) => ({clicks: clicks + 1}));
}
*/
}
createMarkup = value => {
return { __html: value };
};
render() {
const {clicks} = this.state;
return [
<div>Clicks: {clicks}</div>,
<div onClick={this.clickHandler} dangerouslySetInnerHTML={this.createMarkup(string)}/>
];
}
}
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></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>
Here's an example of #2, but don't do this if A) You can solve the underlying problem separately, or B) #1 works:
let string = "Hello <b>Click here</b>";
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
clicks: 0
};
this.divRef = React.createRef();
this.hooked = null;
this.clickHandler = this.clickHandler.bind(this);
}
clickHandler() {
this.setState(({clicks}) => ({clicks: clicks + 1}));
}
hookDivContents() {
// Get the b element
const b = this.divRef.current && this.divRef.current.querySelector("b");
// No-op if it's not there or it's the same element we have hooked
if (!b || b === this.hooked) {
return;
}
// Unhook the old, hook the new
if (this.hooked) {
this.hooked.removeEventListener("click", this.clickHandler);
}
this.hooked = this.divRef.current;
this.hooked.addEventListener("click", this.clickHandler);
}
componentDidMount() {
this.hookDivContents();
}
componentDidUpdate() {
this.hookDivContents();
}
createMarkup = value => {
return { __html: value };
};
render() {
const {clicks} = this.state;
return [
<div>Clicks: {clicks}</div>,
<div ref={this.divRef} dangerouslySetInnerHTML={this.createMarkup(string)}/>
];
}
}
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></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>
Refs are an "escape hatch" giving you direct DOM access. Don't use refs lightly; usually, there's a better choice.
But again: I would solve the underlying problem, whatever it is, differently.
value
inthis.createMarkup(value)
bestring
? – ScrawldangerouslySetInnerHTML
, you're leaving the React scope and you won't be able to call back to it, and even if you get it working, it will be ugly and probably risky. If you want something to be handled by React, do it using React. If you want your<b>
tag to update the state, then, create it properly using React. – Equipollent