Reputation: 1254
In React Hooks documents it is shown how to removeEventListener during the component's cleanup phase. https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
In my use case, I am trying to removeEventListener conditional to a state property of the functional component.
Here's an example where the component is never unmounted but the event listener should be removed:
function App () {
const [collapsed, setCollapsed] = React.useState(true);
React.useEffect(
() => {
if (collapsed) {
window.removeEventListener('keyup', handleKeyUp); // Not the same "handleKeyUp" :(
} else {
window.addEventListener('keyup', handleKeyUp);
}
},
[collapsed]
);
function handleKeyUp(event) {
console.log(event.key);
switch (event.key) {
case 'Escape':
setCollapsed(true);
break;
}
}
return collapsed ? (
<a href="javascript:;" onClick={()=>setCollapsed(false)}>Search</a>
) : (
<span>
<input placeholder="Search" autoFocus />
<a href="javascript:;">This</a>
<a href="javascript:;">That</a>
<input placeholder="Refinement" />
</span>
);
}
ReactDOM.render(<App />, document.body.appendChild(document.createElement('div')));
(Live sample at https://codepen.io/caqu/pen/xBeBMN)
The problem I see is that the handleKeyUp
reference inside removeEventListener
is changing every time the component renders. The function handleKeyUp
needs a reference to setCollapsed
so it must be enclosed by App
. Moving handleKeyUp
inside useEffect
also seems to fire multiple times and lose the reference to the original handleKeyUp
.
How can I conditionally window.removeEventListener using React Hooks without unmounting the component?
Upvotes: 86
Views: 103028
Reputation: 11
useEffect(() => {
if (collapsed) {
return;
}
function handleKeyUp(event) {
switch (event.key) {
case "Escape":
setCollapsed(true);
break;
}
}
window.addEventListener("keyup", handleKeyUp);
return () => window.removeEventListener("keyup", handleKeyUp);
}, [collapsed]);
Upvotes: 0
Reputation: 112917
You can put the handleKeyUp
function inside of the function given to useEffect
(which is the recommended way of doing it according to the official documentation) and only add the listener and return a cleanup function when collapsed
is false.
useEffect(() => {
if (collapsed) {
return;
}
function handleKeyUp(event) {
switch (event.key) {
case "Escape":
setCollapsed(true);
break;
}
}
window.addEventListener("keyup", handleKeyUp);
return () => window.removeEventListener("keyup", handleKeyUp);
}, [collapsed]);
Upvotes: 105
Reputation: 2506
Tholle's answer may work, but it's bad practice to declare a function inside an if
.
It makes it harder to follow when the function is declared and when it is not. Also it can lead to bugs because functions are hoisted up.
There's a neater way to fix it:
By wrapping your event handler with the useCallback hook.
const [collapsed, setCollapsed] = useState(true)
const handleKeyUp = useCallback((event) => {
if (event.key === "Escape") {
setCollapsed(true)
}
}, [setCollapsed])
useEffect(() => {
if (!collapsed) {
window.addEventListener("keyup", handleKeyUp)
} else {
window.removeEventListener("keyup", handleKeyUp)
}
return () => window.removeEventListener("keyup", handleKeyUp)
}, [collapsed, handleKeyUp])
useCallback
has a dependency on setCollapsed
. This makes sure handleKeyUp
is not redefined when the component rerenders (which always happens when state changes)useEffect
will conditionally add/remove the event listener, otherwise events will keep firing as long as the component is mounted.If you use a lot of event handlers in useEffect, there's a custom hook for that: https://usehooks.com/useEventListener/
Here's the question posters example updated with my solution: https://codepen.io/publicJorn/pen/eYzwENN
Upvotes: 76