papryk
papryk

Reputation: 464

React state always returning previous (or initial) state inside callback from fabricjs

The code below is my minimal issue reproduce component. It initializes fabric canvas, and handles "mode" state. Mode state determines whether canvas can be edited and a simple button controls that state.

The problem is that even if mode,setMode works correctly (meaning - components profiler shows correct state after button click, also text inside button shows correct state), the state returned from mode hook inside fabric event callback, still returns initial state.

I suppose that the problem is because of the function passed as callback to fabric event. It seems like the callback is "cached" somehow, so that inside that callback, all the states have initial values, or values that were in state before passing that callback.

How to make this work properly? I would like to have access to proper, current state inside fabric callback.

const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);
const [mode, setMode] = useState("freerun");
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const modes = ["freerun", "edit"];

React.useEffect(() => {
    const canvas = new fabric.Canvas(canvasRef.current, {
      height: 800,
      width: 800,
      backgroundColor: 'yellow'
    });

    canvas.on('mouse:down', function (this: typeof canvas, opt: fabric.IEvent) {
      const evt = opt.e as any;
      console.log("currentMode", mode) // Not UPDATING - even though components profiler shows that "mode" state is now "edit", it still returns initial state - "freerun".
      if (mode === "edit") {
         console.log("edit mode, allow to scroll, etc...");
      }
    });

    setCanvas(canvas);
    return () => canvas.dispose();
}, [canvasRef])

const setNextMode = () => {
  const index = modes.findIndex(elem => elem === mode);
  const nextIndex = index + 1;
  if (nextIndex >= modes.length) {
    setMode(modes[0])
  } else {
    setMode(modes[nextIndex]);
  }
}

return (
<>
  <div>
    <button onClick={setNextMode}>Current mode: { mode }</button>
  </div>
  {`Current width: ${width}`}
  <div id="fabric-canvas-wrapper">
    <canvas ref={canvasRef} />
  </div>
</>
)

Upvotes: 5

Views: 2474

Answers (2)

papryk
papryk

Reputation: 464

That's true! It worked now, thanks Marco.

Now to not run setCanvas on each mode change, I ended up creating another useEffect hook to hold attaching canvas events only. The final code looks similar to this:

const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);
const [mode, setMode] = useState("freerun");
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const modes = ["freerun", "edit"];

  React.useEffect(() => {
    if (!canvas) {
      return;
    }

    // hook for attaching canvas events
    fabric.Image.fromURL(gd, (img) => {
      if (canvas) {
        canvas.add(img)
        disableImageEdition(img);
      }
    });


    canvas.on('mouse:down', function (this: typeof canvas, opt: fabric.IEvent) {
      const evt = opt.e as any;
      console.log("currentMode", mode) // works correctly now
      if (mode === "edit") {
         console.log("edit mode, allow to scroll, etc...");
      }
    });

  }, [canvas, mode])

React.useEffect(() => {
    const canvas = new fabric.Canvas(canvasRef.current, {
      height: 800,
      width: 800,
      backgroundColor: 'yellow'
    });

    setCanvas(canvas);
    return () => canvas.dispose();
}, [canvasRef])

const setNextMode = () => {
  const index = modes.findIndex(elem => elem === mode);
  const nextIndex = index + 1;
  if (nextIndex >= modes.length) {
    setMode(modes[0])
  } else {
    setMode(modes[nextIndex]);
  }
}

return (
<>
  <div>
    <button onClick={setNextMode}>Current mode: { mode }</button>
  </div>
  {`Current width: ${width}`}
  <div id="fabric-canvas-wrapper">
    <canvas ref={canvasRef} />
  </div>
</>
)

I also wonder if there are more ways to solve that - is it possible to solve this using useCallback hook?

Upvotes: 1

Marco Nisi
Marco Nisi

Reputation: 1251

The problem is that mode is read and it's value saved inside the callback during the callback's creation and, from there, never update again.

In order to solve this you have to add mode on the useEffect dependencies. In this way each time that mode changes React will run again the useEffect and the callback will receive the updated (and correct) value.

Upvotes: 3

Related Questions