Я пытаюсь нарисовать изображение на холсте, используя React и Fabric.js.
Демо (CodeSandBox)
В приведенной выше демонстрации при нажатии кнопки «Нарисовать изображение» можно было бы ожидать, что изображение будет немедленно нарисовано на холсте, но изображение на холсте не рисуется.
Причина, по которой изображение не рисуется, по-видимому, заключается в том, что ток useRef
равен нулю при выполнении функции рисования изображения на холсте. (Если условие isInput в строке 50 удалено, containerRef.current
больше не равно нулю, и изображение может рисовать на холсте.)
Не могли бы вы помочь мне нарисовать изображение на холсте?
Код выглядит следующим образом.
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.info(`[${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;
Я понимаю, что containerRef.current
равно нулю, если нет DOM.
Однако,
containerRef
, рисуется setIsInput(true)
containerRef.current
полученное внутри useEffect
не равно нулю.Исходя из вышеизложенного, мы можем ожидать, что containerRef.current
не равно нулю даже внутри функции drawImg.
Вызов setIsInput(true)
просто запрашивает ререндеринг компонента. Это произойдет в ближайшее время, но обычно нет происходит немедленно и синхронно. Таким образом, ваш код в строке после setIsInput(true)
не может предполагать, что ссылка уже обновлена.
Обычно вы делаете это в два этапа. Во-первых, перерисуйте компонент, во-вторых, нарисуйте изображение на холсте. Это можно сделать, поместив код рисования в useEffect, поскольку, как вы заметили, это происходит после заполнения ссылки (потому что это происходит после того, как рендеринг был сброшен в дом):
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]);
Поскольку вы используете реакцию 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();
});
};
Обязательно ознакомьтесь с примечаниями в документация по флеш-синхронизации, потому что возможны последствия, если вы решите его использовать.