I'm trying to add an event listener on the window
when an object is clicked, and then remove that event listener when the object is again clicked.
When a Card
component is clicked, state isCardMoving
is toggled on or off.
I added a useEffect
to watch isCardMoving
. When isCardMoving
is toggled on, it should add a mousemove
event listener to the window that triggers the handleCardMove
function. This function just logs the coordinates of the mouse.
if I click the card again, isCardMoving
will be false, and I would expect the event listener on the window to be removed inside the useEffect
.
What happens, though, is that the event listener will get added when isCardMoving
is true
, and then will not be removed once isCardMoving
is false
.
import React from 'react';
const App = () => {
const [isCardMoving, setIsCardMoving] = React.useState(false);
React.useEffect(() => {
if (isCardMoving) window.addEventListener('mousemove', handleCardMove);
else window.removeEventListener('mousemove', handleCardMove);
}, [isCardMoving]);
const handleCardMove = (event) => console.log({ x: event.offsetX, y: event.offsetY });
return <Card onClick={() => setIsCardMoving(!isCardMoving)} />;
};
I then tried to set a ref
on the window thinking that maybe I would need a previous reference to the window for some reason:
import React from 'react';
const App = () => {
const [isCardMoving, setIsCardMoving] = React.useState(false);
const windowRef = React.useRef(window); // add window ref
// update window ref whenever window is updated
React.useEffect(() => {
windowRef.current = window;
}, [window]);
React.useEffect(() => {
// add and remove event listeners on windowRef
if (isCardMoving) windowRef.current.addEventListener('mousemove', handleCardMove);
else windowRef.current.removeEventListener('mousemove', handleCardMove);
}, [isCardMoving]);
const handleCardMove = (event) => console.log({ x: event.offsetX, y: event.offsetY });
return <Card onClick={() => setIsCardMoving(!isCardMoving)} />;
};
This seems to have the same effect as before.
You can't remove an event listener like this in React or other virtual DOM-based applications. Due to the nature of virtual DOMs libraries, you must remove the event listener in the unmount lifecycle, which is in React hooks and is available within the useEffect
itself. So you have to do it like below with the return keyword. It will do the same thing as componentWillUnmount
in class base components:
React.useEffect(() => {
if (isCardMoving) window.addEventListener("mousemove", handleCardMove);
return () => window.removeEventListener("mousemove", handleCardMove);
}, [isCardMoving]);
Working Demo:
As @ZacharyHaber said in the comments, the main reason behind this behaviour is that your handleCardMove
function will be redefined on every render, so to overcome this situation, we need to unbind the event from the window on each render with the useEffect
callback. You can also make your initial code work using the useCallback
approach. Still, you also need to add the previous useEffect
callback to your component to make sure the event listener will remove in the component unmount cycle, it is a bit more coding, but this one will do the same as the above approach.
const handleCardMove = React.useCallback((event) => {
console.log({ x: event.offsetX, y: event.offsetY });
}, []);
React.useEffect(() => {
if (isCardMoving) window.addEventListener("mousemove", handleCardMove);
else window.removeEventListener("mousemove", handleCardMove);
return () => window.removeEventListener("mousemove", handleCardMove);
}, [isCardMoving, handleCardMove]);
Working Demo: