Home > Software design >  useState for mouse down event not working with Canvas
useState for mouse down event not working with Canvas

Time:11-26

I am making a drawing app with React and Canvas, however if I set the isMouseDown variable as useState instead of useRef, the canvas is not drawing and I cannot seem to find why. Here is the component:

`

function App() {
  const [isMouseDown, setIsMouseDown] = useState(false);
  const isMouseDownRef = useRef(false);
  const canvasRef = useRef(null);
  const ctx = useRef(null);

  function triggerMouseDown() {
    setIsMouseDown(true);
    //isMouseDownRef.current = true;
  }

  function triggerMouseUp() {
    setIsMouseDown(false);
    //isMouseDownRef.current = false;
  }

  useEffect(() => {
    if(canvasRef.current) {
      ctx.current = canvasRef.current.getContext("2d");
      canvasRef.current.width = 720;
      canvasRef.current.height = 480;
      canvasRef.current.addEventListener("mousemove", (e) => {
        draw(
          e.clientX - canvasRef.current.getBoundingClientRect().left, 
          e.offsetY - canvasRef.current.getBoundingClientRect().top
        )
      })

      canvasRef.current.addEventListener("mousedown", triggerMouseDown)

      window.addEventListener("mouseup", triggerMouseUp)
    }

    return () => {
      canvasRef.current.removeEventListener("mousedown", triggerMouseDown);
      window.removeEventListener("mouseup", triggerMouseUp)
    }
  }, [])

  function draw(x, y) {
    if(isMouseDown) {
      ctx.current.beginPath();
      ctx.current.fillStyle = "blue";
      ctx.current.arc(x, y, 20, 0, 2 * Math.PI);
      ctx.current.stroke();
    }
  }
  
  return (
    <div className="App">
      <canvas id="canvas1" ref={canvasRef}></canvas>
      <h1>{JSON.stringify(isMouseDown)}</h1>
    </div>
  );
}

`

I know it's not supposed to be a useState, beacause it would rerender too much, but I am intrested in why it's not working with useState in particular. Thank you!

CodePudding user response:

I made some changes inside your code:

const App = () => {
  const [mouseData, setMouseData] = React.useState({ x: 0, y: 0 });
  const canvasRef = React.useRef(null);
  const ctx = React.useRef(null);

  React.useEffect(() => {
    if(canvasRef.current) {
      ctx.current = canvasRef.current.getContext("2d");
      canvasRef.current.width = 720;
      canvasRef.current.height = 480;
    }

  }, [canvasRef])

  function Draw(e) {      
      ctx.current.beginPath();
      ctx.current.fillStyle = "red";
      ctx.current.arc(mouseData.x, mouseData.y, 20, 0, 2 * Math.PI);
      ctx.current.stroke();
  }
  const SetPos = (e) => {
    setMouseData({
        x: e.clientX,
        y: e.clientY,
    });
};


  return (
    <div className="App">
      <canvas id="canvas1" ref={canvasRef}
      onm ouseEnter={(e) => SetPos(e)}
      onm ouseMove={(e) => SetPos(e)}
      onm ouseDown={(e) => SetPos(e)}
      onm ouseMove={(e) => Draw(e)}
      ></canvas>
      <h1>{JSON.stringify(mouseData)}</h1>
    </div>
  );
}

ReactDOM.createRoot(
    document.getElementById("root")
).render(
    <App />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

CodePudding user response:

Y_T has the right idea in this answer. There's no need for useEffect, but if you do use one, it should use isMouseDown in the capture group so that it triggers adding/removing the move listener at the appropriate time. clearRect is optional. Something like:

useLayoutEffect(() => {
  if (!mouseDown) {
    return;
  }

  const canvas = canvasRef.current;
  const ctx = canvas.getContext("2d");
  const onm ouseMove = e => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.beginPath();
    ctx.arc(e.offsetX, e.offsetY, 20, 0, 2 * Math.PI);
    ctx.stroke();
  };
  canvas.addEventListener("mousemove", onm ouseMove);

  return () => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    canvas.removeEventListener("mousemove", onm ouseMove);
  };
}, [mouseDown]);

But since React gives onMouseMove, we can just use that instead of managing it ourselves:

const App = () => {
  const [mouseDown, setMouseDown] = React.useState(false);
  const canvasRef = React.useRef(null);

  const handleMouseMove = e => {
    if (!mouseDown) {
      return;
    }

    const rect = e.target.getBoundingClientRect();
    const offsetX = e.clientX - rect.left;
    const offsetY = e.clientY - rect.top;
    const canvas = canvasRef.current;
    const ctx = canvas.getContext("2d");
    //ctx.clearRect(0, 0, canvas.width, canvas.height); // optional
    ctx.beginPath();
    ctx.arc(offsetX, offsetY, 20, 0, 2 * Math.PI);
    ctx.stroke();
  };

  return (
    <canvas
      onm ouseDown={() => setMouseDown(true)}
      onm ouseUp={() => setMouseDown(false)}
      onm ouseMove={handleMouseMove}
      ref={canvasRef}
      width="720"
      height="480"
    ></canvas>
  );
}

ReactDOM.createRoot(document.querySelector("#app"))
  .render(<App />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="app"></div>

  • Related