Fix Matter.js Engine Leaks in React Strict Mode
React Strict Mode intentionally mounts, unmounts, and remounts components in development to detect unhandled side effects. When integrating Matter.js, this double-mount behavior frequently creates duplicate physics engines, orphaned animation frames, and multiple canvas elements stacked on top of each other. This article provides a direct solution to isolate and properly clean up Matter.js instances within React lifecycle hooks to prevent memory leaks and ghost physics simulations.
The Cause of the Leak
When Matter.Render.run() and
Matter.Runner.run() are invoked inside a
useEffect hook, they register active loops with the browser
via requestAnimationFrame. If the effect returns without
explicitly stopping these loops, releasing the engine, and removing the
injected <canvas> element, the first instance
continues executing in memory even after React discards the component's
virtual DOM representation.
The Solution:
Comprehensive Cleanup in useEffect
To completely eliminate engine leaks, you must manage
Engine, Render, Runner, and the
canvas element within the cleanup return function of
useEffect.
import React, { useEffect, useRef } from 'react';
import Matter from 'matter-js';
const PhysicsComponent = () => {
const sceneRef = useRef(null);
useEffect(() => {
// 1. Module aliases
const { Engine, Render, Runner, Bodies, Composite } = Matter;
// 2. Create engine and world
const engine = Engine.create();
const world = engine.world;
// 3. Create renderer
const render = Render.create({
element: sceneRef.current,
engine: engine,
options: {
width: 800,
height: 600,
wireframes: false,
},
});
Render.run(render);
// 4. Create and start runner
const runner = Runner.create();
Runner.run(runner, engine);
// 5. Add bodies
const box = Bodies.rectangle(400, 200, 80, 80);
const ground = Bodies.rectangle(400, 590, 810, 60, { isStatic: true });
Composite.add(world, [box, ground]);
// 6. Cleanup function to prevent leaks on unmount/remount
return () => {
// Stop the renderer loop
Render.stop(render);
// Stop the engine runner loop
Runner.stop(runner);
// Clear all bodies and constraints from the world
Composite.clear(world, false);
// Clear the engine state
Engine.clear(engine);
// Remove the automatically generated canvas element from the DOM
if (render.canvas) {
render.canvas.remove();
}
// Clear texture cache to free GPU memory
render.textures = {};
};
}, []);
return <div ref={sceneRef} />;
};
export default PhysicsComponent;Critical Steps Breakdown
- Stop the Runner: Calling
Runner.stop(runner)immediately cancels the internalrequestAnimationFrameloop driving the physics calculations. - Stop the Renderer: Calling
Render.stop(render)stops the rendering cycle so that Matter.js stops drawing frames. - Clear World and Engine:
Composite.clear(world, false)clears all physics bodies and constraints, andEngine.clear(engine)resets the update cycle and cached collision pairs. - Remove the Canvas:
Render.create()appends a<canvas>element to the target DOM element. React unmounting does not inherently remove child elements appended directly via native DOM APIs by third-party libraries. Callingrender.canvas.remove()ensures no ghost canvases persist across re-renders.