Sync React State with Matter.js Without Re-renders
Integrating real-time 2D physics engines like Matter.js into React
requires bridging two fundamentally different paradigms: React's
reactive render cycle and Matter.js's continuous 60 FPS mutation loop.
Storing body transforms (such as position, angle, and velocity) in
standard React state triggers component re-renders on every physics
tick, causing severe performance degradation. You can achieve smooth,
bidirectional synchronization between dynamic React values and Matter.js
body transforms without re-rendering by leveraging mutable React
references (useRef), decoupled game loops, and direct DOM
or Canvas updates.
The Problem with React State in Physics Loops
React's useState schedules a component re-render every
time its setter is called. A standard physics engine updates at 60 Hz or
higher. If a body's position.x and position.y
are copied to React state inside a tick event, React attempts to
re-render the component tree 60 times per second per object. This
introduces reconciliation overhead, frame drops, and input lag.
To maintain peak performance, the physics simulation and the visual representation must bypass the React reconciliation lifecycle entirely.
The Solution: The Mutable Reference Pattern
The core technique relies on React’s useRef hook. A ref
persists across renders without triggering a re-render when its
.current property is mutated.
You can handle synchronization in two directions:
- React to Matter.js: Pass dynamic parameters (e.g., user inputs, scale, wind, target positions) into the physics simulation.
- Matter.js to UI: Reflect body transforms onto the screen (DOM or Canvas) using direct mutations.
1. Passing React Values to Matter.js via Refs
When an external React state changes (such as a slider controlling gravity or user mouse coordinates), mirror that value into a ref. The physics loop reads from this ref on every tick without requiring the component running the physics simulation to re-render.
import React, { useEffect, useRef, useState } from 'react';
import Matter from 'matter-js';
export const PhysicsContainer = () => {
const [speedMultiplier, setSpeedMultiplier] = useState(1);
// Store dynamic state in a ref for the physics loop
const speedRef = useRef(speedMultiplier);
useEffect(() => {
speedRef.current = speedMultiplier;
}, [speedMultiplier]);
// Physics engine setup continues below...2. Updating Matter.js Transforms from Refs
Hook into the Matter.js update cycle using
Matter.Events.on(engine, 'beforeUpdate', callback). Inside
this hook, apply your transformations directly to the target
Matter.Body.
useEffect(() => {
const { Engine, Bodies, Composite, Events } = Matter;
const engine = Engine.create();
const box = Bodies.rectangle(100, 100, 50, 50);
Composite.add(engine.world, box);
// Listen to engine updates to inject values without re-renders
Events.on(engine, 'beforeUpdate', () => {
// Read the latest state from the ref safely
const currentSpeed = speedRef.current;
// Apply transforms directly to the body
Matter.Body.setVelocity(box, {
x: box.velocity.x * currentSpeed,
y: box.velocity.y
});
});
// Start engine runner...
}, []);3. Rendering Body Transforms Without Component Updates
If you are rendering your physics bodies using HTML/DOM elements
instead of the built-in Matter.Render canvas, synchronize
the DOM directly via element refs rather than React state.
import React, { useEffect, useRef } from 'react';
import Matter from 'matter-js';
export const DOMPhysicsBox = () => {
const domElementRef = useRef(null);
const engineRef = useRef(Matter.Engine.create());
useEffect(() => {
const { Engine, Runner, Bodies, Composite, Events } = Matter;
const engine = engineRef.current;
const body = Bodies.rectangle(200, 200, 60, 60);
Composite.add(engine.world, body);
const runner = Runner.create();
Runner.run(runner, engine);
// Sync transforms directly to the DOM node per frame
Events.on(engine, 'afterUpdate', () => {
if (!domElementRef.current) return;
const { x, y } = body.position;
const angle = body.angle;
// Direct mutation bypasses React reconciliation entirely
domElementRef.current.style.transform =
`translate3d(${x - 30}px, ${y - 30}px, 0px) rotate(${angle}rad)`;
});
return () => {
Runner.stop(runner);
Engine.clear(engine);
};
}, []);
return (
<div
ref={domElementRef}
style={{
position: 'absolute',
top: 0,
left: 0,
width: 60,
height: 60,
backgroundColor: 'royalblue',
willChange: 'transform',
}}
/>
);
};Bidirectional Sync Strategies
- Using
will-change: transform: When binding transforms directly to DOM elements via refs, applywill-change: transformin CSS to promote the element to its own GPU compositing layer. - Batching External State Updates: If external UI
components must know about physics events (such as collision
counts or health updates), throttle updates using
requestAnimationFrameor limit state dispatches to discrete events (likecollisionStart) rather than per-frame continuous updates. - Manual Position Overrides: When teleporting or
snapping bodies based on user interactions (e.g., dragging an item), use
Matter.Body.setPosition(body, targetPos)andMatter.Body.setVelocity(body, { x: 0, y: 0 })inside your input event listeners to clear residual momentum.