I am creating a fabric canvas and buttons that instantiate shapes that should be selectable. I don't understand why my component is being re-rendered twice in the following scenario. Because of this my fabric shapes are not selectable. However when I remove <React.StrictMode>
from my index.tsx file, rendering occurs once and my shapes are selectable. I could remove <React.StrictMode>
but I don't believe that is the best solution. Demo below:
const { Fragment, StrictMode, useEffect, useRef } = React;
const { createRoot } = ReactDOM;
const styles = {};
const CanvasComponent = ({ id }) => {
const canvasRef = useRef(null);
useEffect(() => {
console.log('init canvas'); // displayed twice with <React.StrictMode>
canvasRef.current = initCanvas();
}, []);
const initCanvas = () => (
canvasRef.current = new fabric.Canvas(`canvas-${id}`, {
width: 800,
height: 400,
})
);
const addShape = (shapeType: string) => {
let shape: fabric.Object;
switch (shapeType) {
case 'circle':
shape = new fabric.Circle({ radius: 30, fill: 'red', left: 100, top: 100 });
break;
case 'rectangle':
shape = new fabric.Rect({ width: 60, height: 70, fill: 'green', left: 100, top: 100 });
break;
default:
return;
}
canvasRef.current.add(shape);
};
return (
<div>
<button onClick={() => addShape('circle')}>Add Circle</button>
<button onClick={() => addShape('rectangle')}>Add Rectangle</button>
<div className={styles.canvasContainer}>
<canvas id={`canvas-${id}`}></canvas>
</div>
</div>
);
}
function StrictModeEnabled() {
return <StrictMode><h1>Strict Mode Enabled</h1><CanvasComponent id={1} /></StrictMode>;
}
function StrictModeDisabled() {
return <Fragment><h1>Strict Mode Disabled</h1><CanvasComponent id={2} /></Fragment>;
}
const strictModeEnabledRoot = createRoot(document.getElementById("strict-mode-enabled"));
strictModeEnabledRoot.render(<StrictModeEnabled />);
const strictModeDisabledRoot = createRoot(document.getElementById("strict-mode-disabled"));
strictModeDisabledRoot.render(<StrictModeDisabled />);
<script crossorigin src="https://www.unpkg.com/fabric@5.3.0/dist/fabric.js"></script>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="strict-mode-enabled"></div>
<div id="strict-mode-disabled"></div>
Why useEffect running twice and how to handle it well in React? has great answers describing why this is happening and a generalised solution.
In your case, you need to clean up the instantiated canvas. I'm not familiar with Fabric, however from reading the documentation the dispose
method seems suitable:
dispose() → {fabric.Canvas}
Clears a canvas element and removes all event listeners
You'll need to return a function from the useEffect
that calls the above method. It is good practice to return a function that does cleanup from useEffect
s, as elaborated in the linked question. A working demo is also below:
const { Fragment, StrictMode, useEffect, useRef } = React;
const { createRoot } = ReactDOM;
const styles = {};
const CanvasComponent = ({ id }) => {
const canvasRef = useRef(null);
useEffect(() => {
console.log('init canvas'); // displayed twice with <React.StrictMode>
canvasRef.current = initCanvas();
return () => canvasRef.current.dispose();
}, []);
const initCanvas = () => (
canvasRef.current = new fabric.Canvas(`canvas-${id}`, {
width: 800,
height: 400,
})
);
const addShape = (shapeType: string) => {
let shape: fabric.Object;
switch (shapeType) {
case 'circle':
shape = new fabric.Circle({ radius: 30, fill: 'red', left: 100, top: 100 });
break;
case 'rectangle':
shape = new fabric.Rect({ width: 60, height: 70, fill: 'green', left: 100, top: 100 });
break;
default:
return;
}
canvasRef.current.add(shape);
};
return (
<div>
<button onClick={() => addShape('circle')}>Add Circle</button>
<button onClick={() => addShape('rectangle')}>Add Rectangle</button>
<div className={styles.canvasContainer}>
<canvas id={`canvas-${id}`}></canvas>
</div>
</div>
);
}
function StrictModeEnabled() {
return <StrictMode><h1>Strict Mode Enabled</h1><CanvasComponent id={1} /></StrictMode>;
}
const strictModeEnabledRoot = createRoot(document.getElementById("strict-mode-enabled"));
strictModeEnabledRoot.render(<StrictModeEnabled />);
<script crossorigin src="https://www.unpkg.com/fabric@5.3.0/dist/fabric.js"></script>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="strict-mode-enabled"></div>