React useCallback function doesn't get current context value

Viewed 915

I am trying to implement a use case with React Hooks and React Context API with mouse events.

I want to add mousemove event for a container. If user moves over an object (rectangle), a dispatch action is called and context value is updated. I want to achieve that the action is not dispatched repeatedly by checking context value before dispatching. The issue is that function doesn't get current context value.

This the event function useMouseEvents.js

import * as React from "react";

import { DragToCreateContext, actionTypes } from "./reducer";

export function useMouseEvents(elRef) {
  const { dragToCreate, dispatchDragToCreate } = React.useContext(
    DragToCreateContext
  );

  console.log("out of callback", dragToCreate);

  const handleMouseMove = React.useCallback(
    (evt) => {
      if (evt.target.tagName === "P") {
        console.log("inside callback", dragToCreate);
      }
      if (evt.target.tagName === "P" && dragToCreate.sourceNodeId === null) {
        console.log("dispatch");
        dispatchDragToCreate({
          type: actionTypes.ACTIVATE,
          sourceNodeId: 1
        });
      }
    },
    [dragToCreate]
  );

  React.useEffect(() => {
    const el = elRef?.current;
    if (el) {
      el.addEventListener("mousemove", handleMouseMove);
      return () => {
        el.addEventListener("mousemove", handleMouseMove);
      };
    }
  }, [elRef, handleMouseMove]);
}

codesandbox.io

If you hover over rectangle, you will see in console log:

enter image description here

inside callback {sourceNodeId: null}
dispatch 
out of callback {sourceNodeId: 1}
inside callback {sourceNodeId: null}
dispatch 
out of callback {sourceNodeId: 1}
inside callback {sourceNodeId: 1}
inside callback {sourceNodeId: null}

but it should be

inside callback {sourceNodeId: null}
dispatch 
out of callback {sourceNodeId: 1}
inside callback {sourceNodeId: 1}
2 Answers

The behaviour that you see is because your listeners on mouseMove are removed and added whenever your context value changes. Also since your listener is recreated in useEffect it might so happen that before a new listener is attached, an old one executes and you get an old value from the closure.

To solve such scenarios, you can make use of a ref to keep track of updated context values and use that inside your listener callback. This way you will be able to avoid addition and removal of mouse event listener

import * as React from "react";

import { DragToCreateContext, actionTypes } from "./reducer";

export function useMouseEvents(elRef) {
  const { dragToCreate, dispatchDragToCreate } = React.useContext(
    DragToCreateContext
  );

  console.log("out of callback", dragToCreate);
  const dragToCreateRef = React.useRef(dragToCreate);
  React.useEffect(() => {
    dragToCreateRef.current = dragToCreate;
  }, [dragToCreate]);

  const handleMouseMove = React.useCallback((evt) => {
    if (evt.target.tagName === "P") {
      console.log("inside callback", dragToCreateRef.current);
    }
    if (
      evt.target.tagName === "P" &&
      dragToCreateRef.current.sourceNodeId === null
    ) {
      console.log("dispatch");
      dispatchDragToCreate({
        type: actionTypes.ACTIVATE,
        sourceNodeId: 1
      });
    }
  }, []);

  React.useEffect(() => {
    const el = elRef?.current;
    if (el) {
      el.addEventListener("mousemove", handleMouseMove);
      return () => {
        el.addEventListener("mousemove", handleMouseMove);
      };
    }
  }, [elRef, handleMouseMove]);

}

Working codesandbox demo

I can't really find the answers I was hoping for but here's a couple things for you and maybe anyone else wanting to write an answer:

The inside callback {sourceNodeId: null} tells me that something is causing the Context to reset to it's initial values and the way you used your Provider is pretty atypical to what I usually see (changing this didn't really seem to fix anything though).

I thought maybe the useContext inside of useMouseEvents is getting just the default context, but I tried moving things around to guarentee that wasn't the case but that didn't seem to work. (Someone else might want to retry this?)

Edit: Removed this suggestion

Kinda unrelated to the issue, but you're going to want to change your useEffect too:

  React.useEffect(() => {
    const el = elRef?.current;
    if (el) {
      el.addEventListener("mousemove", handleMouseMove);
      return () => {
        el.removeEventListener("mousemove", handleMouseMove);
      };
    }
Related