Apologies if this is a bit rough, I’m relatively new to React. This may even be more of a JavaScript question.
I’m trying to add an event listener that will trigger a callback inside a component. This component can appear on the page multiple times, and with the code below, when #btn
is clicked the console.log
will be output once – I can add as many <MyComponent />
as I like, and the log will only ever be output once – as required.
const callback = (e) => {
console.log('callback happened!!', e.type);
}
const MyComponent = () => {
const btn = document.getElementById('btn');
if (btn) {
const name = 'Bob';
btn.addEventListener('click', callback);
}
return (
<div>
<p>Hi from my component!</p>
</div>
)
}
class App extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div>
<MyComponent />
<p>...</p>
<MyComponent />
</div>
)
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
<div id="btn">button</div>
The problem I’m getting is if I try passing a variable (name
) to the callback function using bind (eg btn.addEventListener('click', callback.bind(null, name));
), when I click the button, the output will be logged twice – not what I want to happen! I need the name
variable and the event object to be available in the callback
function.
I should clarify that in the example above I am using a click listener on the button, but the real-world case will be listening for an event emitted from something else.
I have tried moving the callback function into the App
class, and passing it as a prop to the component, but the same thing happens – as soon as I bind a variable to it, it triggers the console log twice.
So, my questions are, why does this happen? How can these requirements be achieved?
All suggestions welcome, and thanks!
2
Answers
The problem here is that you are creating the event listener when your component gets created, but you’re never deleting it. This means that each time you instantiate the component, a new event listener is created, and so a new console.log will be displayed.
Furthermore, you are doing things in a strange way:
<div id="btn">button</div>
is.I will propose you two solutions to this problem, the first one using Class Components, in order to be closer to your solution (even though it’s not recommended to use them anymore) and one with functional components:
Class Components:
Functional Components:
Note that in both the solutions I don’t explicitly create the event listener, but I attach the callback function that has to be invoked on click inside the
onClick
property of the element.If, on the other hand, you are obliged to add the event listener, then you it’s important to remove it when the components unmount.
Class Components:
Functional Components:
Hope this is useful!
You’ve said the button is a stand-in for a different kind of event source, so we won’t worry about the fact that attaching a click handler like this isn’t how you would normally do this in React. I’m going to assume the other event source is outside the React tree.
Your component function is called every time the component needs to be rendered, which can be multiple times. Your code is adding a handler every time the function runs. When you do that using
callback
directly, it’s the same function every time so it doesn’t get added (becauseaddEventListener
doesn’t add the same function for the same event to the same event target more than once, even if you call it repeatedly). But when you usebind
, you’re creating a new function every time, and soaddEventListener
adds those new functions on every render.Instead, do the set up only when the component is mounted. Also, remove it when the component is unmounted. You can do that via
useEffect
: