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!
Top comments (8)
Even better: put the
onKeyDown
function insideReact.useEffect
. ThenuseCallback
is not needed.As Kent C Dodds suggests: "If you must define a function for your effect to call, then do it inside the effect callback, not outside." For more info see "Needlessly externally defined functions" on epicreact.dev/myths-about-useeffect
And it is also mentioned in the React docs: reactjs.org/docs/hooks-faq.html#is...
thanks for mentioning
I was thinking the same thing reading this article glad you commented it.
Solved my confussion, thanks : )
saved my time , thank you
i forgot the [] in the end of useCallback
Nice post, Marco.
I wasn't aware of this after using hooks for a year!
Just joined DEV to thank you - wasted almost 6hrs on this with no result until this post.
Have a good day & Thank You
Amazing!!!!