How to (really) remove eventListeners in React

marcostreng

Marco Streng

Posted on March 27, 2020

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!

💖 💪 🙅 🚩
marcostreng
Marco Streng

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