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:

Structuring the Custom Hook

A well-structured hook should:

  1. Provide a DOM ref for the container element.
  2. Initialize the Engine, Render, and Runner modules inside a useEffect.
  3. Expose engine references so the consuming component can add bodies or constraints.
  4. 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:

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.