Search code examples
reactjshtml5-canvasfabricjsuse-effect

Mouseover event for Fabric JS object added with React useEffect doesn't trigger


I'm trying to capture mouseover events for fabric object added some time after the canvas was initialized. I'm using react, so I'm doing that in a useEffect hook.

  useEffect(() => {
    canvasRef.current = new fabric.Canvas("c", {
      width: 300,
      height: 600,
      backgroundColor: "#666",
      selection: false
    });

    canvasRef.current.add(
      new fabric.Rect({
        width: 100,
        height: 100,
        top: 100,
        left: 100,
        fill: "#660000"
      })
    );
    canvasRef.current.on("mouse:over", function (e) {
      console.log("mouse over", e.e.clientX, e.e.clientY, e.target);
    });


    canvasRef.current.requestRenderAll();

    setTimeout(() => {
      canvasRef.current.add(
        new fabric.Rect({
          width: 100,
          height: 100,
          top: 200,
          left: 100,
          fill: "#666600"
        })
      );
      canvasRef.current.requestRenderAll();
    }, 1000);
  }, []);

For some reason, only the object added immediately after the canvas was initialized responds to the mouse:over event.

The other 2 rectangles added later (one with timeout, the other in a useEffect hook based on change of state) do not log the target when hovering.

I'm not sure if this is because fabric JS interacts in a strange way with react or something else, but I'd appreciate if someone can point me in the right direction here.

See this codesandbox for the full code:

https://codesandbox.io/s/react-fiberjs-mouseevent-test-k95zuq?file=/src/App.js

See this example that doesn't use React but uses timeouts, and somehow the hover function works properly:

https://codepen.io/zinkkrysty/pen/ExEMjLo?editors=0011


Solution

  • https://reactjs.org/docs/strict-mode.html

    With Strict Mode starting in React 18, whenever a component mounts in development, React will simulate immediately unmounting and remounting the component:

    So useEffect is called twice. You should properly deinitialize the component. For example:

    useEffect(() => {
      ...
      return () => canvasRef.current.dispose();
    }, [])