How to (really) remove eventListeners in React
Marco Streng
Posted on March 27, 2020
Sometimes you need to track user interaction like e.g. scrolling or events like the change of the window size. In this cases you will add an eventListener
to your window
/document
/body
or whatever.
When working with eventListeners you always have to take care about cleaning them up, if the component doesn't need them anymore or gets unmounted.
Mount & Unmount
A common and simple use case is to add a listener after the initial mount and remove it when the component unmounts. This can be done with the useEffect hook.
Example:
const onKeyDown = (event) => { console.log(event) }
useEffect(() => {
window.addEventListener('keydown', onKeyDown)
return () => { window.removeEventListener('keydown', onKeyDown) }
}, [])
❗️Don't forget the second parameter []
when calling useEffect
. Otherwise it will run on every render.
State change or property change
What work's perfect in the example above, won't work when you add and remove listeners depending on a state or prop change (as i had to learn).
Example:
// ⚠️ This will not work!
const [isVisible, setVisibility] = useState(false)
const onKeyDown = (event) => { console.log(event) }
handleToggle((isVisible) => {
if (isVisible) window.addEventListener('keydown', onKeyDown)
else window.removeEventListener('keydown', onKeyDown)
})
return (
<button onClick={() => setVisibility(!isVisible)}>Click me!</button>
)
After clicking the button the second time the eventListner should be removed. But that's not what will happen.
But why?
The removeEventListener(event, callback)
function will internally do an equality check between the given callback and the callback which was passed to addEventListener()
. If this check doesn't return true no listener will be removed from the window.
But we pass in the exact same function to addEventListener()
and removeEventListener()
! 🤯
Well,... not really.
As React renders the component new on every state change, it also assigns the function onKeyDown()
new within each render. And that's why the equality check won't succeed.
Solution
React provides a nice Hook called useCallback(). This allows us to memoize a function and the equality check will succeed.
Example
const [isVisible, setVisibility] = useState(false)
const onKeyDown = useCallback((event) => { console.log(event) }, [])
handleToggle((isVisible) => {
if (isVisible) window.addEventListener('keydown', onKeyDown)
else window.removeEventListener('keydown', onKeyDown)
})
return (
<button onClick={() => setVisibility(!isVisible)}>Click me!</button>
)
❗️Again: Don't forget the second parameter []
when calling useCallback()
. You can pass in an Array of dependencies here, to control when the callback should change. But that's not what we need in our case.
—
If you got any kind of feedback, suggestions or ideas - feel free to comment this blog post!
Posted on March 27, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 14, 2022