0

I was trying to implement a simple paint component with React hooks. My expected behavior was 'mouseMove' to be executed when I moved my mouse while remaining clicked. However, state.isMouseDown always returned false within mouseMove().

Any fixes or references to potentially helpful documents would be grateful.

const initialState = {
  isMouseDown: false,
  isMouseMoving: false
};

const DrawingCanvas = () => {
  const [state, setState] = useState(initialState);

  useEffect(() => {
    console.log('mounted');
    document.addEventListener('mousedown', () => mouseDown());
    document.addEventListener('mousemove', () => mouseMove());
  }, []);

  const mouseDown = () => {
    console.log('mousedown');
    setState(state => ({
      ...state,
      isMouseDown: true
    }));
  };

  const mouseMove = () => {
    // why is this false even when click and move?
    console.log('mouseMove:isMouseDown', state.isMouseDown);

    if (!state.isMouseDown) return;
    console.log('mousemove'); // this line is not being executed
    setState(state => ({
      ...state,
      isMouseMoving: true
    }));
  };

  console.log(state);
  return (
    <div>
      <p>mouseDown: {`${state.isMouseDown}`}</p>
      <p>mouseMoving: {`${state.isMouseMoving}`}</p>
    </div>
  );
};

1 Answers1

1

As explained in this related answer, the problem is that event listener accesses state object from the scope where it was defined, i.e. initial state, because event is listened on component mount.

A solution is to either use mutable state, or access state exclusively from state updater function. In the code above, state.isMouseDown refers to original state. In case it's needed to avoid state updates, state updater can return original state:

  const mouseMove = () => {
    setState(state => {
      if (!state.isMouseDown)
        return state; // skip state update
      else
        return {
          ...state,
          isMouseMoving: true
        };
    });
  };
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • That's a really neat solution! I posted an answer that would have caused an infinite loop and I was just changing it when I read your answer - elegant! – Tom Finney Mar 12 '19 at 16:51
  • 1
    @TomFinney There's more than one way to handle this situation. Reattaching listeners like you suggested is definitely one of them. It provides some overhead but it can be ignored in case it benefits the component in other ways. It just needs to be handled differently, listeners need to be set up separately so there's a meaningful effect input, `useEffect(() => { document.addEventListener('mousedown', mouseDown); return () => { document.removeEventListener('mousedown', mouseDown); }, [state.isMouseDown])` – Estus Flask Mar 12 '19 at 17:00