How to use Matter.js in a React functional component
Posted on February 18, 2021 in
4 min read
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!