React Hook for Matter.js Engine Setup and Cleanup
Integrating the Matter.js 2D physics engine into a React application requires synchronizing physical simulations with the React component lifecycle. This guide explains how to build a robust, reusable custom hook that initializes the Matter.js Engine, Runner, and Renderer, attaches them cleanly to the DOM, and thoroughly disposes of resources when the component unmounts to prevent memory leaks and animation glitches.
The Challenges of Integrating Matter.js in React
Matter.js relies on direct DOM manipulation and its own
requestAnimationFrame loop. In React, several common issues
arise if this lifecycle is not managed carefully:
- Duplicate Canvases: React 18's Strict Mode mounts, unmounts, and remounts components in development, frequently resulting in multiple stacked canvas elements.
- Orphaned Loops: Failing to stop the runner or renderer leaves CPU-intensive loops executing in the background.
- Dangling Physics Bodies: Retaining references to the engine or world prevents garbage collection.
Structuring the Custom Hook
A well-structured hook should:
- Provide a DOM
reffor the container element. - Initialize the
Engine,Render, andRunnermodules inside auseEffect. - Expose engine references so the consuming component can add bodies or constraints.
- Execute a teardown sequence in the effect's cleanup callback.
Implementation
import { useEffect, useRef } from 'react';
import Matter from 'matter-js';
export function useMatter(options = {}) {
const sceneRef = useRef(null);
const engineRef = useRef(null);
useEffect(() => {
const container = sceneRef.current;
if (!container) return;
const { Engine, Render, Runner, Composite } = Matter;
// 1. Create Engine
const engine = Engine.create(options.engineOptions || {});
engineRef.current = engine;
// 2. Create Renderer
const render = Render.create({
element: container,
engine: engine,
options: {
width: container.clientWidth || 800,
height: container.clientHeight || 600,
wireframes: false,
background: 'transparent',
...options.renderOptions,
},
});
// 3. Create Runner
const runner = Runner.create();
// 4. Start Engine and Renderer
Render.run(render);
Runner.run(runner, engine);
// Optional: Run custom setup callback (e.g., adding bodies)
if (typeof options.onInit === 'function') {
options.onInit({ engine, render, runner });
}
// 5. Cleanup on unmount
return () => {
// Stop execution
Render.stop(render);
Runner.stop(runner);
// Clear world and engine
Composite.clear(engine.world, false);
Engine.clear(engine);
// Remove canvas safely from the DOM
if (render.canvas && render.canvas.parentNode) {
render.canvas.parentNode.removeChild(render.canvas);
}
// Clear references
render.canvas = null;
render.context = null;
render.textures = {};
engineRef.current = null;
};
}, []);
return { sceneRef, engineRef };
}Step-by-Step Breakdown of the Lifecycle
1. Initialization
The hook uses sceneRef to target the mounting
<div>. Inside useEffect,
Engine.create() sets up the core physics pipeline, and
Render.create() generates an HTML5
<canvas> inside the target container.
Runner.create() provides the decoupled game loop that
coordinates physics updates independently of rendering.
2. Execution
Calling Render.run(render) begins drawing physics bodies
to the canvas, while Runner.run(runner, engine)
synchronizes the engine calculations with the browser's refresh
rate.
3. Cleanup Sequence
The return function of useEffect must unwind every
Matter.js subsystem in the correct order:
Render.stop(render): Cancels the activerequestAnimationFrameloop responsible for drawing.Runner.stop(runner): Halts the internal physics simulation loop.Composite.clear(engine.world, false): Empties all bodies, constraints, and composites from the world. Setting the second argument tofalseensures only child objects are cleared without destroying the root composite structure prematurely.Engine.clear(engine): Clears update events and internal object pools.- DOM Removal: The canvas created by Matter.js is explicitly removed from the container to prevent duplicate elements on re-render.
- Nullifying References: Clearing cached textures and canvas contexts enables complete garbage collection.
Consuming the Hook in a Component
import React from 'react';
import Matter from 'matter-js';
import { useMatter } from './useMatter';
export function PhysicsView() {
const { sceneRef } = useMatter({
onInit: ({ engine }) => {
const { Bodies, Composite } = Matter;
const box = Bodies.rectangle(400, 200, 80, 80);
const ground = Bodies.rectangle(400, 590, 810, 60, { isStatic: true });
Composite.add(engine.world, [box, ground]);
},
});
return (
<div
ref={sceneRef}
style={{ width: '100%', height: '600px', overflow: 'hidden' }}
/>
);
}This pattern encapsulates low-level imperative Matter.js operations, prevents memory leaks across component lifecycles, and maintains compatibility with React's rendering pipeline.