简体   繁体   English

useRef.current 存在,但是是 null 取决于 scope

[英]useRef.current exists, but is null depending on scope

I am trying to draw an image on a canvas using React and Fabric.js.我正在尝试使用 React 和 Fabric.js 在 canvas 上绘制图像。
Demo(CodeSandBox) 演示(代码沙盒)

In the above demo, when the "Draw image" button is pressed, one would expect an image to be immediately drawn on the canvas, but no image is drawn on the canvas.在上面的演示中,当按下“绘制图像”按钮时,人们会期望立即在 canvas 上绘制图像,但在 canvas 上没有绘制任何图像。

参考当前空

The reason why the image is not drawn seems to be that the current of useRef is null when executing the function to draw the image on the canvas.(If the isInput condition on line 50 is removed, containerRef.current is no longer null and the image can be drawn on the canvas.)画不出来的原因好像是执行function在canvas上画图时,useRef的current是useRef (如果去掉第50行的isInput条件, containerRef.current就不再是null了,图片可在canvas上绘制。)

Could you help me to draw an image on the canvas?你能帮我在canvas上画个图吗?


The code is as follows.代码如下。

const App = () => {
  const [img, setImg] = useState(defaultImg);
  const changeImg = ({ target: { value } }: { target: { value: string } }) =>
    setImg(value);

  const [isInput, setIsInput] = useState(false);
  const containerRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);

  const checkCurrent = (caller: string) => {
    console.log(`[${caller}]Container: ${containerRef.current}`);
  };
  useEffect(() => {
    if (!isInput) return;

    checkCurrent("useEffect");
  }, [isInput]);

  const drawImg = (e: { preventDefault: () => void }) => {
    e.preventDefault();
    if (!img) return;
    setIsInput(true);

    checkCurrent("drawImg");

    canvas = new fabric.Canvas("canvas");
    fabric.Image.fromURL(img, (imgObj: fabric.Image) => {
      canvas.add(imgObj).renderAll();
    });
  };

  return (
    <div className="App">
      {!isInput && (
        <form onSubmit={drawImg}>
          Demo image url
          <input type="text" onChange={changeImg} defaultValue={img} />
          <button type="submit">Draw image</button>
        </form>
      )}

      {isInput && (
        <>
          <button onClick={() => setIsInput(false)}>Reset</button>
          <div ref={containerRef} className="canvas_container">
            <canvas ref={canvasRef} id="canvas" />
          </div>
        </>
      )}
    </div>
  );
};

export default App;

I understand that containerRef.current is null if there is no DOM.我知道如果没有 DOM, containerRef.current是 null。

However,然而,

  1. the div to which containerRef is set is drawn by setIsInput(true)设置containerRef的 div 由setIsInput(true)绘制
  2. containerRef.current obtained inside useEffect is not null. useEffect里面得到的containerRef.current不是null。

From the above points, we can expect that containerRef.current is not null even inside the drawImg function.从以上几点可以看出,即使在drawImg function内部, containerRef.current也不是null。

Calling setIsInput(true) just requests that react rerender the component.调用setIsInput(true)只是请求重新渲染组件。 This will happen soon, but it generally not happen immediately and synchronously.这将很快发生,但通常不会立即同步发生。 So your code on the line after setIsInput(true) cannot assume that the ref has been updated yet.因此,您在setIsInput(true)之后的行中的代码不能假定 ref 已经更新。

So typically, you would do this in two steps.所以通常,您会分两步执行此操作。 First, rerender the component, second draw the image to the canvas. This can be done by putting the drawing code into a useEffect since, as you've noticed, this happens after the ref is populated (because it happens after the render has been flushed to the dom):首先,重新渲染组件,然后将图像绘制到 canvas。这可以通过将绘图代码放入 useEffect 来完成,因为正如您所注意到的,这发生在填充 ref 之后(因为它发生在渲染完成之后)刷新到 dom):

const drawImg = (e: { preventDefault: () => void }) => {
  e.preventDefault();
  if (!img) return;
  setIsInput(true);
}

useEffect(() => {
  if (!isInput) return;

  const canvas = new fabric.Canvas("canvas");
  fabric.Image.fromURL(img, (imgObj: fabric.Image) => {
    canvas.add(imgObj).renderAll();
  });
}, [isInput]);

Since you're using react 18 there is one additional option to mention, though i recommend that you don't use this unless you really need it.由于您使用的是 React 18,所以还有一个额外的选项需要提及,但我建议您不要使用它,除非您确实需要它。 React 18 adds a new function called flushSync which can force state updates to be rendered synchronously. React 18 添加了一个名为flushSync的新 function,它可以强制同步呈现 state 更新。

import { flushSync } from 'react-dom'; // Note: 'react-dom', not 'react'

// ...

const drawImg = (e: { preventDefault: () => void }) => {
  e.preventDefault();
  if (!img) return;

  flushSync(() => {
    setIsInput(true);
  });

  canvas = new fabric.Canvas("canvas");
  fabric.Image.fromURL(img, (imgObj: fabric.Image) => {
    canvas.add(imgObj).renderAll();
  });
};

Make sure to check out the notes in the documentation for flush sync , because there are consequences if you choose to use it.请务必查看文档中有关 flush sync的注释,因为如果您选择使用它会产生后果。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM