Why React StrictMode drew every stroke twice
My whiteboard drew every line twice, but only in development. StrictMode wasn’t the bug. It was showing me an effect that set things up and never cleaned them up.

On this page
The bug
StormDraw is a multiplayer whiteboard: you draw on a canvas, and every stroke goes over a WebSocket to everyone else in the room. While building it, every line I drew showed up twice. Not offset, exactly on top of itself, which made it easy to miss until undo seemed to do nothing.
The strange part: the deployed version was fine. Only npm run dev drew double.
What StrictMode actually does
In development, React 18’s <StrictMode> mounts every component, unmounts it straight away, and mounts it again. Effects run, their cleanups run, then the effects run a second time. It does this on purpose: a component that breaks when mounted twice will also break when a real user leaves a page and comes back.
Where my code went wrong
The board was a plain class that attached mouse listeners to the canvas and a message listener to the socket. I created it inside an effect and never tore it down:
useEffect(() => {
const board = new Board(canvasRef.current, socket);
// Board adds mouse listeners and a socket "message" listener
}, [socket]);After StrictMode’s mount, unmount and mount, there were two Boards, each with its own mousemove handler and socket listener. One stroke, two draws.
The fix
Give the board a way to undo everything it set up, and return that from the effect:
useEffect(() => {
const board = new Board(canvasRef.current, socket);
return () => board.destroy();
}, [socket]);destroy() {
this.canvas.removeEventListener('mousedown', this.onDown);
this.canvas.removeEventListener('mousemove', this.onMove);
window.removeEventListener('mouseup', this.onUp);
this.socket.removeEventListener('message', this.onMessage);
}Why I’m glad it happened
Turning StrictMode off would have “fixed” it too. But the leak was real: without the cleanup, every time someone left a room and joined another, the old board kept listening. StrictMode made it show up on day one instead of in a bug report.
What I’d tell past me
If an effect adds, opens, subscribes to or creates something, it returns a function that removes, closes, unsubscribes from or destroys it.
- Event listeners →
removeEventListener - WebSockets and subscriptions → close or unsubscribe
- Timers →
clearInterval/clearTimeout - Canvas or chart instances → their own
destroy()