useRef.current 存在,但根据范围为空
useRef.current exists, but is null depending on scope
我正在尝试使用 React 和 Fabric.js 在 canvas 上绘制图像。
Demo(CodeSandBox)
在上面的演示中,当按下“绘制图像”按钮时,人们会期望立即在 canvas 上绘制图像,但在 canvas 上没有绘制图像。
画不出来的原因好像是在canvas上执行画图的函数时useRef
的current为null。(如果isInput条件在线50 被删除,containerRef.current
不再为 null,可以在 canvas 上绘制图像。)
你能帮我在canvas上画个图吗?
代码如下
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;
我理解如果没有DOM,containerRef.current
为空。
然而,
- 设置
containerRef
的div由setIsInput(true)
绘制
containerRef.current
里面得到的useEffect
不为空
从以上几点可以看出,即使在 drawImg 函数内部,containerRef.current
也不为 null。
调用 setIsInput(true)
只是请求 React 重新渲染组件。这很快就会发生,但通常 不会 立即同步发生。所以你在 setIsInput(true)
之后的代码不能假定 ref 已经更新。
通常,您会分两步完成此操作。首先,重新渲染组件,其次将图像绘制到 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]);
由于您使用的是 React 18,因此还有一个选项需要提及,但我建议您不要使用它,除非您确实需要它。 React 18 添加了一个名为 flushSync
的新函数,它可以强制同步呈现状态更新。
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();
});
};
请务必查看 documentation for flush sync 中的注释,因为如果您选择使用它会产生后果。
我正在尝试使用 React 和 Fabric.js 在 canvas 上绘制图像。
Demo(CodeSandBox)
在上面的演示中,当按下“绘制图像”按钮时,人们会期望立即在 canvas 上绘制图像,但在 canvas 上没有绘制图像。
画不出来的原因好像是在canvas上执行画图的函数时useRef
的current为null。(如果isInput条件在线50 被删除,containerRef.current
不再为 null,可以在 canvas 上绘制图像。)
你能帮我在canvas上画个图吗?
代码如下
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;
我理解如果没有DOM,containerRef.current
为空。
然而,
- 设置
containerRef
的div由setIsInput(true)
绘制 containerRef.current
里面得到的useEffect
不为空
从以上几点可以看出,即使在 drawImg 函数内部,containerRef.current
也不为 null。
调用 setIsInput(true)
只是请求 React 重新渲染组件。这很快就会发生,但通常 不会 立即同步发生。所以你在 setIsInput(true)
之后的代码不能假定 ref 已经更新。
通常,您会分两步完成此操作。首先,重新渲染组件,其次将图像绘制到 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]);
由于您使用的是 React 18,因此还有一个选项需要提及,但我建议您不要使用它,除非您确实需要它。 React 18 添加了一个名为 flushSync
的新函数,它可以强制同步呈现状态更新。
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();
});
};
请务必查看 documentation for flush sync 中的注释,因为如果您选择使用它会产生后果。