1

I am attempting to add and remove an event listener within a functional React component. The listener is added fine but is not removed when asked to be. I believe the issue is that the function I am referencing handlemousemove is recreated every component render and so when removeEventListener attempts to remove it, it's not the same function reference as when addEventListener added it.

I tried moving handlemousemove out of the component but it required access to the setState hooks generated in the component.

const handleMouseMove = e => {
    setYOffset(e.clientY-280)
    setXOffset(e.clientX-350)
}

const followMouse = () => {
    if (isFollowingMouse){
        setIsFollowingMouse(false)
        document.removeEventListener("mousemove", handleMouseMove)
    } else {
        setIsFollowingMouse(true)
        document.addEventListener("mousemove", handleMouseMove)
    }
}

...

<button name="mouse" onClick={followMouse}>
    Follow Mouse
</button>

All branches of execution are hit here but document.removeEventListener("mousemove", handleMouseMove) doesn't actually remove the event listener.

Is there a way to have a "static method" within a functional component? Is that even the issue here?

Here's a link to code sandbox with the whole code: https://codesandbox.io/s/pzrwh

3 Answers 3

2

The old way to do it was with render props, but now that hooks have arrived this is a better solution

const MyComponent = (props) => {
    const [isFollowingMouse, setIsFollowingMouse] = React.useState(false);
    const [xOffset, setXOffset] = React.useState(0);
    const [yOffset, setYOffset] = React.useState(0);
    
    const handleMouseMove = e => {
        if (isFollowingMouse) {
            setYOffset(e.clientY-28);
            setXOffset(e.clientX-35);
        }
    };

    const followMouse = () => {
        setIsFollowingMouse(!isFollowingMouse);
    }

    const styles = {
        'cat': {
            'backgroundColor': 'red',
            'height': '20px',
            'position': 'absolute',
            'left': xOffset,
            'top': yOffset,
            'width': '20px',
            'display': isFollowingMouse ? 'block' : 'none'
        }
    };

    return (
      <div style={{ 'height': '100%' }} onMouseMove={handleMouseMove}>
        <div style={ styles.cat }>C</div>
        <button name="mouse" onClick={followMouse}>
          Follow Mouse
        </button>
      </div>
    )
}



ReactDOM.render(<MyComponent />, document.getElementById('root'));
html,
body,
#root {
    height: 100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks that was really helpful. I got it working by using a custom hook like in the linked article by just conditionally using the position from the hook if isFollowing was true and nothing otherwise.
0

I think your description of the issue is spot on. A quick fix is to define the variable handleMouseMove outside of your App function - essentially making the variable static and not recreated every render.

Then, within the body of the function, only assign the handleMouseMove variable if it's currently unassigned, and set it back to null when you set isFollowingMouse to false.

Comments

0

With React 16.7 you can use Hooks to do this:

import React, { useCallback, useEffect, useState } from 'react';

const DraggedComponent = React.memo(
    props => {
        const [isFollowingMouse, setIsFollowingMouse] = useState(false);
        const [xOffset, setXOffset] = useState(0);
        const [yOffset, setYOffset] = useState(0);

        const handleMouseMove = useCallback(
            e => {
                if (isFollowingMouse) {
                    setYOffset(e.clientY-28);
                    setXOffset(e.clientX-35);
                }
            }, [isFollowingMouse, setYOffset, setXOffset]
        );

        useEffect(
            () => {
                document.addEventListener('mousemove', handleMouseMove);
                return () => document.removeEventListener('mousemove', handleMouseMove);
            },
            [handleKeyDown]
        );

        const followMouse = () => setIsFollowingMouse(!isFollowingMouse);

        return (
            <div onMouseMove={handleMouseMove}>
                <div>C</div>
                <button name="mouse" onClick={followMouse}>
                    Follow Mouse
                </button>
            </div>
        )
    }
);

ReactDOM.render(<DraggedComponent />, document.getElementById('root'));

In this example React.memo() ensures that the component is only redrawn if state or properties change. Similar useCallback() will cache the event listener for the mousemove event, such that this will not be recreated only if isFollowingMouse, setYOffset or setXOffset change, instead of every rerender. useEffect will be called once the component is created, and once every time the handleMouseMove callback changes. Furthermore it returns a function, which is automatically called if the component is destroyed or the parameter handleKeyDown changes.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.