DEV Community 👩‍💻👨‍💻

Cover image for How to (really) remove eventListeners in React
Marco Streng
Marco Streng

Posted on • Updated on

How to (really) remove eventListeners in React

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) }
  }, [])

Enter fullscreen mode Exit fullscreen mode

❗️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>
  )
Enter fullscreen mode Exit fullscreen mode

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>
  )
Enter fullscreen mode Exit fullscreen mode

❗️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 (4)

Collapse
 
stigkj profile image
Stig KleppeJørgensen

Even better: put the onKeyDown function inside React.useEffect. Then useCallback 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...

Collapse
 
dance2die profile image
Sung M. Kim

Nice post, Marco.

I wasn't aware of this after using hooks for a year!

Collapse
 
gyanisunkara profile image
gyani-sunkara

Just joined DEV to thank you - wasted almost 6hrs on this with no result until this post.
Have a good day & Thank You

Collapse
 
karkranikhil profile image
Nikhil karkra

Amazing!!!!

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.