Integrating Three.js With React Easily Without Wrapper Libraries

Integrating Three.js With React Easily Without Wrapper Libraries

If you’re reading this, I assume that you already know what three.js is and what wonders it can do! If you don’t, then three.js is a javascript library which can help you render 3D objects and scenes on a web browser. Cool, isn’t it?

But there’s no point of such a library if you can’t integrate it with a kickass web framework like React. Well you can. With just a few little tweaks, you can integrate three.js (without any external libraries) to work with React seamlessly.

I’ll talk about these little tweaks in this blog post.

Obviously, you can use third party wrappers like react-three-fiber. If you want to get your project up and running in no time, I would suggest you use react-three-fiber. This blog is for you if you don’t like using any wrapper library or if the little monkey in you wants to know how stuff works at the core.

Why Can’t I Simply Import And Use Three.js Normally In React?

You can simply import three.js and start using it but you’ll face these two problems:

  1. If you follow the official docs, you’ll be manipulating the real dom and not the react-dom so technically your 3D scene would be running on top of the react app. Due to this, your 3D component won’t follow the component hierarchy of your react app.
  2. The other components in your react app won’t be able to interact with the 3D scene. And more importantly you won’t be able to manipulate your 3D scene with any react functionalities like hooks. 

React uses something called as a virtual DOM. I won’t go into detail here, but if you want to know more about the React Virtual Dom, you can read this article.

The scenes created in three.js need to be added to the DOM. You need to call the appendChild function to do so. If you follow the official docs, you’ll add the scene to the dom using a function like this:

document.body.appendChild( renderer.domElement );

And because of this you’ll face the problems stated above.

The Solution

We just need to make a few tweaks to solve the above problems. If you keep following the tutorial, you would be able to: 

  1. Add your 3D scene to the react-dom instead of the real dom.
  2. You would also be able to manipulate your scene using buttons which are defined outside the three.js scene.

How would we achieve this? We would achieve this by linking our 3D scene to a div using React’s useRef functionality. 

So let’s get started!

Basic Setup

First, install a boilerplate react app. I’ll use create-react-app for this. Use the following npm command to get started.

npx create-react-app threejs-react

Next, create a new file ThreeCube.js in the src folder. Add the following starter code to this file:

import React from "react";
 
const ThreeCube = () => {
 return (
   <div>
     <h3>Three Cube</h3>
   </div>
 );
};
 
export default ThreeCube;

Then change the default code in the App.js file and import your ThreeCube component there.

This is how your App.js file should look now:

import React from "react";
import ThreeCube from "./ThreeCube";
 
const App = () => {
 return (
   <div>
     <h1>Three.js-React Integration</h1>
     <ThreeCube />
   </div>
 );
};
 
export default App;

That’s it for our starter code.

Installing Three.js

We are ready to install the three.js package now. Use the following npm command to install three.js:

npm install three

That’s it. We’re done. Now we can use three.js in React.

Creating A 3D Cube

If you run your app now, your screen should look like this:

Integrating Three.js With React Easily Without Wrapper Libraries 1
Starter Code Screenshot (an empty screen with a heading and a subheading)

Our next step is to create a 3D cube. All code we write from now on will go into our ThreeCube.js file.

For simplicity, I’ll just use the getting started code from the official three.js website. You can view the getting started code here.

If we just paste the code in the above example into a useEffect hook in our react app, the code should technically work. So let’s do that.

Let’s add the cube example from the official docs to a useEffect hook in our ThreeCube.js file.

We’ll also need to import THREE from the three library that we installed earlier. 

Modify your ThreeCube.js file so that it looks like this:

import React, { useEffect } from "react";
import * as THREE from "three";
 
const ThreeCube = () => {
 useEffect(() => {
   var scene = new THREE.Scene();
   var camera = new THREE.PerspectiveCamera(
     75,
     window.innerWidth / window.innerHeight,
     0.1,
     1000
   );
 
   var renderer = new THREE.WebGLRenderer();
   renderer.setSize(window.innerWidth, window.innerHeight);
   document.body.appendChild(renderer.domElement);
 
   var geometry = new THREE.BoxGeometry();
   var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
   var cube = new THREE.Mesh(geometry, material);
   scene.add(cube);
 
   camera.position.z = 5;
 
   var animate = function () {
     requestAnimationFrame(animate);
 
     cube.rotation.x += 0.01;
     cube.rotation.y += 0.01;
 
     renderer.render(scene, camera);
   };
 
   animate();
 }, []);
 
 return (
   <>
     <div></div>
   </>
 );
};
 
export default ThreeCube;

This is how our web app should be looking like at the moment:

threejs working in real-dom

Even though this works, this is not what we want. As I discussed in the previous section, using three.js like this has a few problems.

I’ll demonstrate these problems so you can get a better understanding of why we are not using this method to run a 3D scene in our react app.

If we add a h2 tag in our App.js file like this:

 <div>
     <h1>Three.js-React Integration</h1>
     <ThreeCube />
     <h2>Test Headline</h2>
  </div>

We just modified the return statement in the App.js file and added a h2 tag after the ThreeCube component.

We would expect the output to be

–H1
–3D Scene
–H2

But instead this is our output:

three.js working in real dom instead of react-dom
Unexpected output because three.js is working on real dom, not the react-dom

The ThreeCube component doesn’t adhere to our react hierarchy because it isn’t a part of the react-dom.

Secondly, we also won’t be able to manipulate the 3D scene (like changing the length of the cube) using a button that we define in our react app.

I hope you understand the problem now. So let’s get started on the solution. To solve this problem we will use React’s useRef hook. useRef helps us to access a child component imperatively.

We will add a reference (ref) to a div and we’ll append our scene to this div using the useRef hook.

After making these updates, this is how your ThreeCube component should look like:

import React, { useEffect, useRef } from "react";
import * as THREE from "three";
 
const ThreeCube = () => {
 const cubeRef = useRef(null);
 useEffect(() => {
   var scene = new THREE.Scene();
   var camera = new THREE.PerspectiveCamera(
     75,
     window.innerWidth / window.innerHeight,
     0.1,
     1000
   );
 
   var renderer = new THREE.WebGLRenderer();
   renderer.setSize(window.innerWidth, window.innerHeight);
   cubeRef.current.appendChild(renderer.domElement);
 
   var geometry = new THREE.BoxGeometry();
   var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
   var cube = new THREE.Mesh(geometry, material);
   scene.add(cube);
 
   camera.position.z = 5;
 
   var animate = function () {
     requestAnimationFrame(animate);
 
     cube.rotation.x += 0.01;
     cube.rotation.y += 0.01;
 
     renderer.render(scene, camera);
   };
 
   animate();
 }, []);
 
 return (
   <>
     <div ref={cubeRef}></div>
   </>
 );
};
 
export default ThreeCube;

Now, if we run our app. The output should be as expected. Our cube component is a part of the react-dom now.

three.js scene added to the react-dom
three.js scene added to the react-dom now

Setting The Scene Dimensions According To The Div

Now we’ll add dimensions to our div tag and use these dimensions for our scene. This way, we won’t be dependent on the window screen size.

To do this, just update the camera variable and the renderer size.

Modified camera variable:

var camera = new THREE.PerspectiveCamera(
     75,
     cubeRef.current.clientWidth / cubeRef.current.clientWidth,
     0.1,
     1000
   );

Modified renderer variable:

var renderer = new THREE.WebGLRenderer();
   renderer.setSize(
     cubeRef.current.clientWidth,
     cubeRef.current.clientHeight
   );

Next, give some dimensions to the div tag.

Modified return statement:

return (
   <>
     <div
       ref={cubeRef}
       style={{ width: "90%", height: "600px", margin: "40px" }}
     ></div>
   </>
 );

You can simply change the dimensions of the div and the dimension of the 3D scene will automatically change. You can even use a styling library like bootstrap to set the width of the div and the 3D scene will adapt to your div.

The app should look like this now:

ThreeCube scene with dimensions of the div
The 3D scene takes the dimensions of the div tag

Changing Cube Dimensions With A Button

Next, we’ll add buttons through which we will change the dimensions of the cube.

We’ll once again use the useRef hook to create a controls reference.

Take a look at the code below to get a better understanding.

This is the final code for the ThreeCube.js file:

import React, { useEffect, useRef } from "react";
import * as THREE from "three";
 
const ThreeCube = () => {
 const cubeRef = useRef(null);
 const controls = useRef(null);
 useEffect(() => {
   var scene = new THREE.Scene();
   var camera = new THREE.PerspectiveCamera(
     75,
     cubeRef.current.clientWidth / cubeRef.current.clientWidth,
     0.1,
     1000
   );
 
   var renderer = new THREE.WebGLRenderer();
   renderer.setSize(cubeRef.current.clientWidth, cubeRef.current.clientHeight);
   cubeRef.current.appendChild(renderer.domElement);
 
   var geometry = new THREE.BoxGeometry();
   var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
   var cube = new THREE.Mesh(geometry, material);
   scene.add(cube);
 
   camera.position.z = 5;
 
   var animate = function () {
     requestAnimationFrame(animate);
 
     cube.rotation.x += 0.01;
     cube.rotation.y += 0.01;
 
     renderer.render(scene, camera);
   };
 
   animate();
 
   var increaseCubeSize = (incrementValue) => {
     cube.scale.x += incrementValue;
     cube.scale.y += incrementValue;
     cube.scale.z += incrementValue;
   };
 
   var decreaseCubeSize = (decrementValue) => {
     cube.scale.x -= decrementValue;
     cube.scale.y -= decrementValue;
     cube.scale.z -= decrementValue;
   };
 
   controls.current = { increaseCubeSize, decreaseCubeSize };
 }, []);
 
 return (
   <>
     <button
        onClick={() => {
          controls.current.increaseCubeSize(1);
        }}
      >
        Increase Size
      </button>
      <button
        onClick={() => {
          controls.current.decreaseCubeSize(1);
        }}
      >
        Decrease Size
      </button>
     <div
       ref={cubeRef}
       style={{ width: "90%", height: "450px", margin: "40px" }}
     ></div>
   </>
 );
};
 
export default ThreeCube;

This is how the react app finally looks like:

final output of three.js integration with react
Final App Output

As you can see, we created control functions to increase and decrease the cube size inside the useEffect hook (this is where our scene lives). Then we added a ref to these functions using the useRef hook.

Now we can use the controls ref to manipulate the dimensions of the cube from outside the useEffect hook.

This is it for this tutorial. If you want me to create more tutorials like this, let me know in the comments section.

You can view the final code on GitHub.

Please share this article if it helped you. If you’re looking for a remote react developer, feel free to contact me ?

Leave a Reply

3 × 1 =