React 状态总是在来自 fabricjs 的回调中返回先前(或初始)状态

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

下面的代码是我的最小问题重现组件。它初始化结构 canvas,并处理“模式”状态。模式状态决定 canvas 是否可以编辑,一个简单的按钮控制该状态。

问题是即使 mode,setMode 工作正常(意思是 - 组件分析器在单击按钮后显示正确状态,按钮内的文本也显示正确状态),从 mode 钩子内部返回的状态fabric 事件回调,仍然是 returns 初始状态。

我想问题出在作为回调传递给 fabric 事件的函数。似乎回调以某种方式被“缓存”,因此在该回调内部,所有状态都有初始值,或者在传递该回调之前处于状态的值。

如何让它正常工作?我想访问 fabric 回调中正确的当前状态。

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>
</>
)

问题是 mode 被读取,它的值在回调创建期间保存在回调中,并且从那里不再更新。

为了解决这个问题,您必须在 useEffect 依赖项上添加 mode。这样,每次 mode 更改时,React 都会 运行 再次 useEffect 并且回调将接收更新的(和正确的)值。

没错!现在成功了,谢谢 Marco。

现在 不是 运行 setCanvas 在每次模式更改时,我最终创建了另一个 useEffect 挂钩来保持附加 canvas 事件 。最终代码看起来类似于:

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>
</>
)

我也想知道是否有更多方法可以解决这个问题 - 是否可以使用 useCallback 钩子来解决这个问题?