I am trying to draw an image on a canvas using React and Fabric.js.
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.)
Could you help me to draw an image on the 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.
However,
- the div to which
containerRef
is set is drawn bysetIsInput(true)
containerRef.current
obtained insideuseEffect
is not null.
From the above points, we can expect that containerRef.current
is not null even inside the drawImg function.
CodePudding user response:
Calling setIsInput(true)
just requests that react rerender the component. 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.
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):
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 adds a new function called flushSync
which can force state updates to be rendered synchronously.
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.