Search code examples
javascriptreactjsreact-dom

How to remove window eventListener in React component?


This peace of code shows user mouse coordinates, it works properly.

But I also want to remove event handler by clicking the STOP button. Unfortunately, this seems to be wrong somehow because event handler still tracking and changing position even after clicking the button:

import { useEffect, useState } from 'react'

function MouseCords() {
    const [position, setPosition] = useState({ x: 0, y: 0 })

    function mouseMoveHandler(event) {
        setPosition({
            x: event.clientX,
            y: event.clientY
        })
    }

    useEffect(() => {
        window.addEventListener('mousemove', mouseMoveHandler)
    }, [])

    return (
        <div>
            <pre>
                x: {position.x}
                <br />
                y: {position.y}
            </pre>
            <button
                onClick={() =>
                    window.removeEventListener('mousemove', mouseMoveHandler)
                }
            >
                STOP
            </button>
        </div>
    )
}

I guess there might be a problem with context, but I am not sure what to do to solve it.

By the way, this is React component and I am using it in App.js like this:

import MouseCords from './components/MouseCords'

function App() {
    return (
        <div>
            <MouseCords />
        </div>
    )
}

export default App

Solution

  • A function declared at the top level of a component will be redeclared on every render, as will the anonymous function in the button's onClick property. As such, when you try to remove the listener the function reference in the click handler won't match the function reference that the listener was set with and so it won't work.

    The simplest solution is to declare the function using useCallback which will ensure that the function is only declared on first render (due to the empty dependency array) and so the click handler and useEffect function references will match.

    You should also always provide a 'cleanup' function by returning a function from your useEffect that will clean up on unmount any side-effects the useEffect my have caused during the lifecycle of the component.

    const { useState, useEffect, useCallback } = React;
    
    function MouseCoords() {
      const [position, setPosition] = useState({ x: 0, y: 0 });
    
      const mouseMoveHandler = useCallback((event) => {
        setPosition({
          x: event.clientX,
          y: event.clientY
        });
      }, []);
    
      useEffect(() => {
        window.addEventListener('mousemove', mouseMoveHandler);
        
        return () => {
          window.removeEventListener('mousemove', mouseMoveHandler);
        };
      }, []);
    
      return (
        <div>
          <pre>
            x: {position.x}
            <br />
            y: {position.y}
          </pre>
          <button
            onClick={() =>
              window.removeEventListener('mousemove', mouseMoveHandler)
            }
          >
            STOP
          </button>
        </div>
      )
    }
    
    const container = document.getElementById('root');
    const root = ReactDOM.createRoot(container);
    root.render(<MouseCoords />);
    <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
    
    <div id='root'></div>

    Alternatively you could handle the listener entirely through the useEffect and use a boolean flag to conditionally add/remove the listener everytime it changes. In this case the 'cleanup' will run each time there is a change in the dependency array.

    const { useState, useEffect, useCallback } = React;
    
    function MouseCoords() {
      const [position, setPosition] = useState({ x: 0, y: 0 });
      const [shouldTrack, setShouldTrack] = useState(true);
    
      const mouseMoveHandler = useCallback((event) => {
        setPosition({
          x: event.clientX,
          y: event.clientY
        });
      }, []);
    
      useEffect(() => {
        if (shouldTrack) {
          window.addEventListener('mousemove', mouseMoveHandler);
        
          return () => {
            window.removeEventListener('mousemove', mouseMoveHandler);
          };
        }
      }, [shouldTrack]);
    
      return (
        <div>
          <pre>
            x: {position.x}
            <br />
            y: {position.y}
          </pre>
          <button
            onClick={() => setShouldTrack(b => !b)}
          >
            {shouldTrack ? 'Stop' : 'Start'}
          </button>
        </div>
      )
    }
    
    const container = document.getElementById('root');
    const root = ReactDOM.createRoot(container);
    root.render(<MouseCoords />);
    <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
    
    <div id='root'></div>