How to use Matter.js in a React functional component
This time Iâve wanted to integrate Matter.js in a React app.
To integrate Matter.js into a React application effectively, itâs crucial to understand how to manage state within a functional component, especially since Matter.js operates as a stateful engine.
The challenge lies in maintaining Reactâs stateless nature while utilizing Matter.jsâs powerful physics engine. By leveraging useRef instead of useState, you can add statefulness to your component without unnecessary re-renders, ensuring optimal performance.
This approach allows you to initialize the Matter.js engine within a useEffect hook, creating an interactive simulation with mouse-based particle addition.
This method not only simplifies the integration process but also maintains Reactâs efficiency. Whether youâre experimenting with Matter.js on CodePen or building complex React components, understanding this integration is key to creating responsive, physics-based animations in your projects.
The basic Matter.js boilerplace code comes from this codepen that I did to wrap my head with the library.
Itâs a simple example that inits and runs the physic simulation engine with a basic mouse interaction to add particles on press:

In a React functional component we need to deal with its stateless aspect while Matter.js is a statefull engine. We donât want to exploit the React nature to update the DOM on each update. For that reason, useRef instead useState is used to add statefulness to the component.
First off, we keep in memory both the DOM wrapper and the Matter Engine in a ref
import { useEffect, useRef } from 'react'
import { Engine } from 'matter-js'
function Comp(props){
const scene = useRef()
const engine = useRef(Engine.create())
return (
<div ref={scene} style={{ width: '100%', height: '100%' }} />
)
}
export default Comp
Then, we use the useEffect hook to initialize the engine with all the required properties. The engine variable is kept outside the hook because we need it in another handler (more below).
useEffect(() => {
// mount
const cw = document.body.clientWidth
const ch = document.body.clientHeight
const render = Render.create({
element: scene.current,
engine: engine.current,
options: {
width: cw,
height: ch,
wireframes: false,
background: 'transparent'
}
})
// boundaries
World.add(engine.current.world, [
Bodies.rectangle(cw / 2, -10, cw, 20, { isStatic: true }),
Bodies.rectangle(-10, ch / 2, 20, ch, { isStatic: true }),
Bodies.rectangle(cw / 2, ch + 10, cw, 20, { isStatic: true }),
Bodies.rectangle(cw + 10, ch / 2, 20, ch, { isStatic: true })
])
// run the engine
Engine.run(engine.current)
Render.run(render)
// unmount
return () => {
// destroy Matter
Render.stop(render)
World.clear(engine.current.world)
Engine.clear(engine.current)
render.canvas.remove()
render.canvas = null
render.context = null
render.textures = {}
}
}, [])
We change the template a bit to add the interactivity attributes:
return (
<div
onMouseDown={handleDown}
onMouseUp={handleUp}
onMouseMove={handleAddCircle}
>
<div ref={scene} style={{ width: '100%', height: '100%' }} />
</div>
)
and here the actual handlers:
const isPressed = useRef(false)
const handleDown = () => {
isPressed.current = true
}
const handleUp = () => {
isPressed.current = false
}
const handleAddCircle = e => {
if (isPressed.current) {
const ball = Bodies.circle(
e.clientX,
e.clientY,
10 + Math.random() * 30,
{
mass: 10,
restitution: 0.9,
friction: 0.005,
render: {
fillStyle: '#0000ff'
}
})
World.add(engine.current.world, [ball])
}
}
Now the full component at your disposal for a straight copy/paste:
import { useEffect, useRef } from 'react'
import { Engine, Render, Bodies, World } from 'matter-js'
function Comp (props) {
const scene = useRef()
const isPressed = useRef(false)
const engine = useRef(Engine.create())
useEffect(() => {
const cw = document.body.clientWidth
const ch = document.body.clientHeight
const render = Render.create({
element: scene.current,
engine: engine.current,
options: {
width: cw,
height: ch,
wireframes: false,
background: 'transparent'
}
})
World.add(engine.current.world, [
Bodies.rectangle(cw / 2, -10, cw, 20, { isStatic: true }),
Bodies.rectangle(-10, ch / 2, 20, ch, { isStatic: true }),
Bodies.rectangle(cw / 2, ch + 10, cw, 20, { isStatic: true }),
Bodies.rectangle(cw + 10, ch / 2, 20, ch, { isStatic: true })
])
Engine.run(engine.current)
Render.run(render)
return () => {
Render.stop(render)
World.clear(engine.current.world)
Engine.clear(engine.current)
render.canvas.remove()
render.canvas = null
render.context = null
render.textures = {}
}
}, [])
const handleDown = () => {
isPressed.current = true
}
const handleUp = () => {
isPressed.current = false
}
const handleAddCircle = e => {
if (isPressed.current) {
const ball = Bodies.circle(
e.clientX,
e.clientY,
10 + Math.random() * 30,
{
mass: 10,
restitution: 0.9,
friction: 0.005,
render: {
fillStyle: '#0000ff'
}
})
World.add(engine.current.world, [ball])
}
}
return (
<div
onMouseDown={handleDown}
onMouseUp={handleUp}
onMouseMove={handleAddCircle}
>
<div ref={scene} style={{ width: '100%', height: '100%' }} />
</div>
)
}
export default Comp
Happy simulation!