Reputation: 464
The code below is my minimal issue reproduce component. It initializes fabric canvas, and handles "mode" state. Mode state determines whether canvas can be edited and a simple button controls that state.
The problem is that even if mode,setMode
works correctly (meaning - components profiler shows correct state after button click, also text inside button shows correct state), the state returned from mode
hook inside fabric event callback, still returns initial state.
I suppose that the problem is because of the function passed as callback to fabric event. It seems like the callback is "cached" somehow, so that inside that callback, all the states have initial values, or values that were in state before passing that callback.
How to make this work properly? I would like to have access to proper, current state inside fabric callback.
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>
</>
)
Upvotes: 5
Views: 2474
Reputation: 464
That's true! It worked now, thanks Marco.
Now to not run setCanvas
on each mode change, I ended up creating another useEffect
hook to hold attaching canvas events only. The final code looks similar to this:
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>
</>
)
I also wonder if there are more ways to solve that - is it possible to solve this using useCallback
hook?
Upvotes: 1
Reputation: 1251
The problem is that mode
is read and it's value saved inside the callback during the callback's creation and, from there, never update again.
In order to solve this you have to add mode
on the useEffect
dependencies. In this way each time that mode
changes React will run again the useEffect
and the callback will receive the updated (and correct) value.
Upvotes: 3