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

  1. Stop the Runner: Calling Runner.stop(runner) immediately cancels the internal requestAnimationFrame loop driving the physics calculations.
  2. Stop the Renderer: Calling Render.stop(render) stops the rendering cycle so that Matter.js stops drawing frames.
  3. Clear World and Engine: Composite.clear(world, false) clears all physics bodies and constraints, and Engine.clear(engine) resets the update cycle and cached collision pairs.
  4. 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. Calling render.canvas.remove() ensures no ghost canvases persist across re-renders.