Write three.js in React Using react-three-fiber

Robert Borghesi

So you want to write some 3D graphics/animations in your React apps using three.js? Let’s see how we can do just that, leveraging the react-three-fiber library.

This article supposes you are familiar with React (with hooks) and three.js. If not, you can get a quick start on React hooks here and on three.js here.

Getting started & Examples

We’re going to use react-three-fiber, (for now-on called R3F), which is essentially a powerful React renderer for three.js, both for the web and with React Native.

We started using this library for this simple reason: if you’re usually working with React and you want to create a three.js experience, the standard approach can be painful to scale -> Some examples here, here and here.

Working live examples: If you want to have an idea on what you can create using react-three-fiber you can check our last projects here, here and here.

But First, Why react-three-fiber?

  • Component-based Scene: This is actually the main reason why I love this library. It allows us to write three.js objects in a declarative way, so we can build-up our scene creating re-usable React components, leveraging props, states and hooks. Keep in mind that you can write essentially three.js entire object catalogue and all its properties.
  • Built-in helpers: It’s delivered with some useful functions like raycaster and it gives you access on each mesh to all the useful pointer-events like onClick, onPointerOver, onPointerOut,… Exactly like any DOM element.
  • Hooks: It comes with a lot of hooks already, like useFrame that allows us to attach functions into the raf loop (or even override the default one), and useThree from where we can get useful objects like renderer, scene, camera,…
  • Resize: If you don’t need special custom resize logic, it already handles the camera changes for you, and the canvas resizes itself independently. Besides you can even get access to the size of the viewport (the size of the quad you are rendering based on 3D coordinates).
  • Dependency-free: Since it’s “just” a renderer it doesn’t rely on the three.js version, so you are free to choose your preferred version.
  • Re-render only when needed: It works as any React component, it updates itself on a dependency change (state, store, etc).

Let’s Write Some three.js

So, it’s time to give you a quick and simple example. First of all we need to define our Canvas component. Everything inside of it will be added to the main scene (defined by react-three-fiber for us).

In this example I'll emphasize the code splitting in multiple files to show how cool is to work in components. It's not performance-optimized, because it's not the goal of this introduction. You can find the complete example with the final code here.

//...
import { Canvas } from "react-three-fiber";
//...

function App() {
  return (
    <Canvas> // here you can pass a lot of options as prop
      <Children/> // any Threejs object (mesh, group or whatever)
      <Children/> // any Threejs object (mesh, group or whatever)
    </Canvas>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

The first step is done. With just these few lines we’ve already created the canvas, the camera (a perspective one but you can customize it) and the scene and we don’t need to care about the resize. Awesome!

Add a group with just a mesh

Let’s compare the two approaches on how to create a basic mesh and add it into a group:

// plain JS
const group = new Group();
const geo = new BoxBufferGeometry(2,2,2);
const mat = new MeshStandardMaterial({color: 0x1fbeca});
const mesh = new Mesh(geo, mat);
group.position.set(0,0.1,0.1);
group.add(mesh);
scene.add(group);

// declarative
<group position={[0,0.1,0.1]}>
  <mesh>
    <boxBufferGeometry attach="geometry" args={[0.047, 0.5, 0.29]} />
    <meshStandardMaterial attach="material" color={0xf95b3c} />
  </mesh>
</group>
// we don't need to add to the scene because since it's a child of the canvas it's automatically added

As you can see it’s pretty straight-forward. You just need to pass the required arguments as args property and then you can set all the other properties as props.

Since it is supposed that you're already familiar with three.js, I won't compare the two approaches anymore 😉.

Create multiple cubes

Suppose now that you want to create multiple cubes and add them into the group. In plain vanilla JavaScript you need to create a class (or not, depends on your preferred approach) to create and handle them, and push them into an array and so on. With R3F you can just add an array of components into what the function returns, as any DOM-element. You can even pass a prop and apply it internally.

export default () => {
  const nodesCubes = map(new Array(30), (el, i) => {
    return <Cube key={i} prop1={..} prop2={...}/>;
  });

  return (
    <group>
      { nodesCubes }
    </group>
  );
};

Animate them

Let’s now animate them. react-three-fiber provides you a great way to attach your logic into the raf loop, using the useFrame hook.

// ...
import {useFrame} from 'react-three-fiber'
// ...
const mesh = useRef()

useFrame( ({gl,scene,camera....}) => {
  mesh.current.rotation.y += 0.1
})

Handle properties with states

Let’s now suppose you want to change some properties on hover or on click. You don’t need to set any raycaster because it’s already defined for you. Since we are using React we can leverage the useState hook.

//...
const mesh = useRef()
const [isHovered, setIsHovered] = useState(false);
const color = isHovered ? 0xe5d54d : 0xf95b3c;

const onHover = useCallback((e, value) => {
    e.stopPropagation(); // stop it at the first intersection
    setIsHovered(value);
  }, [setIsHovered]);
//...

<mesh 
  ref={mesh}
  position={position}
  onPointerOver={e => onHover(e, true)}
  onPointerOut={e => onHover(e, false)}
>
  <boxBufferGeometry attach="geometry" args={[0.047, 0.5, 0.29]} />
  <meshStandardMaterial color={color} attach="material" />
</mesh>

Let’s now change the color and the animation too, modifying the state of the cube from “active” to “inactive” based on the user’s click. As before we can play with state and use the built-in function onClick.

//...
const [isHovered, setIsHovered] = useState(false);
const [isActive, setIsActive] = useState(false);
const color = isHovered ? 0xe5d54d : (isActive ? 0xf7e7e5 : 0xf95b3c);

  const onHover = useCallback((e, value) => {
    e.stopPropagation();
    setIsHovered(value);
  }, [setIsHovered]);

  const onClick = useCallback(
    e => {
      e.stopPropagation();
      setIsActive(v => !v);
    },
    [setIsActive]
  );

   // raf loop
  useFrame(() => {
    mesh.current.rotation.y += 0.01 * timeMod;
    if (isActiveRef.current) { // a ref is needed because useFrame creates a "closure" on the state
      time.current += 0.03;
      mesh.current.position.y = position[1] + Math.sin(time.current) * 0.4;
    }
  });
//...

return (
  <mesh
    ref={mesh}
    position={position}
    onClick={e => onClick(e)}
    onPointerOver={e => onHover(e, true)}
    onPointerOut={e => onHover(e, false)}
  >
    <boxBufferGeometry attach="geometry" args={[0.047, 0.5, 0.29]} />
    <meshStandardMaterial color={color} attach="material" />
  </mesh>
);

That’s it. Easy-peasy.

Add some lights

As before if we want to add some lights we just need to create a function (or component) and add it into the scene graph.

return (
  <>
    <ambientLight intensity={0.9} />
    <pointLight intensity={1.12} position={[0, 0, 0]} />
  </>
)

Last step

We’ve already created a basic scene in three.js, but let’s do our last step just to show you how easy it is to add custom logic into the render-loop.

Let’s add a rotation on the container group of all the cubes. You can just simply go to the group component, use the useFrame hook and set the rotation there, just like before.

 useFrame(() => {
  group.current.rotation.y += 0.005;
});

That’s all, looks easy and straight-forward right?


Bonus

Since R3F is brought to you by Paul Henschel it’s totally compatible with react-spring. This means you can use react-spring to animate all your three.js stuff, accessing directly to the element, and to be honest it’s totally mind blowing 🤯.

Last Points to Clarify

Since I’ve discovered react-three-fiber, I’ve used it for all my React three.js projects, from the simplest demo to the most complex one. I strongly suggest you to read all the documentation online and overview all the features because here we just covered some of them.

By the way, if you are used to working with three.js, you’ll have to switch a little how you interact / create stuff. It has, as everything, a learning curve and you’ll need some time to change the approach if you want to keep performance at its best, like for example using a shared material, cloning a mesh, etc, but based on my experience it’s absolutely worth the time invested.

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#