Using Matter.js with React and Modern Frameworks

Matter.js is a robust 2D physics engine for the web that integrates seamlessly with modern component-based frameworks like React, Vue, and Svelte. This guide explains how to bridge the gap between declarative UI libraries and the imperative nature of Matter.js, details a standard implementation pattern in React, and covers critical performance considerations to keep your physics simulations running smoothly.

The Challenge: Declarative vs. Imperative

Modern front-end frameworks rely on a declarative programming paradigm, where the UI is a direct reflection of application state managed through a virtual DOM or reactive bindings. Conversely, Matter.js operates imperatively: it instantiates an internal simulation loop, manages its own state via physics bodies, and directly manipulates the DOM or an HTML5 <canvas> element on every tick.

To make them work together, you must encapsulate the Matter.js lifecycle inside the component lifecycle, ensuring the physics engine only initializes once the target DOM element exists and tears down cleanly when the component unmounts.

Implementing Matter.js in React

In React, the connection between the virtual DOM and the Matter.js engine is handled using useRef and useEffect.

import React, { useEffect, useRef } from 'react';
import Matter from 'matter-js';

const PhysicsScene = () => {
  const sceneRef = useRef(null);
  const engineRef = useRef(null);

  useEffect(() => {
    // 1. Module aliases
    const { Engine, Render, Runner, Bodies, Composite } = Matter;

    // 2. Create engine
    const engine = Engine.create();
    engineRef.current = engine;

    // 3. Create renderer
    const render = Render.create({
      element: sceneRef.current,
      engine: engine,
      options: {
        width: 800,
        height: 600,
        wireframes: false,
        background: '#f0f0f0'
      }
    });

    Render.run(render);

    // 4. Create runner
    const runner = Runner.create();
    Runner.run(runner, engine);

    // 5. Add bodies
    const box = Bodies.rectangle(400, 200, 80, 80, { restitution: 0.8 });
    const ground = Bodies.rectangle(400, 590, 810, 60, { isStatic: true });
    Composite.add(engine.world, [box, ground]);

    // 6. Cleanup on unmount
    return () => {
      Render.stop(render);
      Runner.stop(runner);
      Composite.clear(engine.world, false);
      Engine.clear(engine);
      render.canvas.remove();
      render.textures = {};
    };
  }, []);

  return <div ref={sceneRef} />;
};

export default PhysicsScene;

Best Practices for Modern Frameworks

1. Proper Cleanup and Teardown

Failing to destroy instances when a component unmounts leads to memory leaks, multiple running loops, and orphaned <canvas> elements. Always stop the Runner and Render, clear the Composite and Engine, and remove the generated canvas element in your cleanup hook.

2. Avoid Syncing Physics Data with State

Matter.js updates at 60 frames per second. Storing body positions or velocities in React state (useState) will trigger 60 re-renders per second, quickly degrading application performance. If you need to read physics data, use mutable references (useRef), native canvas rendering, or custom event listeners (Matter.Events.on) that update non-state variables.

3. Decouple Physics from Rendering

While Matter.js includes a built-in canvas renderer (Matter.Render), it is primarily intended for debugging and prototyping. In production-grade applications, you can use Matter.js strictly as a calculation engine. You can read the positions and angles of bodies on each tick and render them using custom WebGL renderers (like PixiJS or Three.js) or targeted SVG/HTML updates.

4. Responsiveness and Dynamic Resizing

To make your simulation responsive to container or window size changes, attach a ResizeObserver to the parent container. When the bounds change, update the render.bounds, canvas dimensions, and reposition any static boundary bodies (such as walls and floors) to maintain layout consistency.