How to use plain Three.js in your React apps

Integrate Three.js code in a React application, without using React bindings

Click here to share this article on LinkedIn »

In this post I’m going to show a simple example of integration between plain Three.js and React.js. I won’t be using React bindings for Three.js, they’re great but I think plain Three.js allows for more flexibility and is easier to work with.

I’m going to structure the Three.js code following the Entity/Component pattern presented here, but you don’t need to know how that works to be able to understand this post.

A working example of Three.js code in a React app is available in this repo.

You can download Three.js from npm and add it to your application using npm install three or yarn add three (if you’re using yarn instead of npm).

React component

First of all we need to create the React component that will host the canvas and the Three.js application.

import React, { Component } from 'react';

import threeEntryPoint from './threejs/threeEntryPoint'; export default class ThreeContainer extends Component { componentDidMount() {

threeEntryPoint(this.threeRootElement);

} render () {

return (

<div ref={element => this.threeRootElement = element} />

);

}

}

The div element will be the container of the canvas (I prefer to create the canvas from Javascript, it allows for more flexibility. But you can easily create it here, instead of the div element).

I’m using the ref attribute in order to get a reference to the div element.

I am passing this.threeRootElement (the reference to the div) to the function threeEntryPoint(). This function is called when the function componentDidMount() is automatically called by React, when the component is mounted.

Now let’s see what threeEntryPoint actually is.

Leaving the React world

threeEntryPoint is a function that lives outside of React and doesn’t care about it. For what it knows it could be called from any web app, built with any framework or no framework at all.

threeEntryPoint is a function that takes a dom element as parameter, this element will be the container of the canvas.

The function threeEntryPoint is responsible for:

Creating the canvas Binding events Creating Three.js objects Starting the render loop

import SceneManager from './SceneManager'; export default containerElement => {

const canvas = createCanvas(document, containerElement);

const sceneManager = new SceneManager(canvas);



bindEventListeners();

render(); function createCanvas(document, containerElement) {

const canvas = document.createElement(‘canvas’);

containerElement.appendChild(canvas);

return canvas;

} function bindEventListeners() {

window.onresize = resizeCanvas;

resizeCanvas();

} function resizeCanvas() {

canvas.style.width = ‘100%’;

canvas.style.height= ‘100%’; canvas.width = canvas.offsetWidth;

canvas.height = canvas.offsetHeight; sceneManager.onWindowResize();

} function render(time) {

requestAnimationFrame(render);

sceneManager.update();

}

}

SceneManager is the object responsible for the Three.js logic. It is the entry point to the Three.js part of the app (You can learn more about SceneManager in my Entity/Component post).

You can see in resizeCanvas() that the canvas has 100% width and height. Doing so the canvas will fill its container element. The container can be easily resized with CSS like you would normally do in React with any element.

Three.js

SceneManager is a function that contains the actual Three.js code.

As you can see in the snippet below this function (in this case the function doesn’t have a name, but it is a SceneManager) takes in a canvas and returns an object. The object exposes the functions that have to be visible outside, i.e. the public functions.

(I haven’t copy-pasted all the Three.js code, because it’s quite basic and is easy to understand what each function does by reading its name.

If you’re interested in the details, you can find the full implementation and explanation in my Entity/Component post.)

import * as THREE from 'three'; export default canvas => {

const scene = buildScene();

const renderer = buildRender(screenDimensions);

const camera = buildCamera(screenDimensions);

const sceneSubjects = createSceneSubjects(scene); function buildScene() { //... } function buildRender({ width, height }) { //... } function buildCamera({ width, height }) { //... } function createSceneSubjects(scene) { //... } function update() { //... } function onWindowResize() { //... } return {

update,

onWindowResize

}

}

This pattern for private and public function is just a personal choice. Maybe it is easier to visualize by looking at the following snippet:

export default params => { function publicMethod() { //... } function privateMethod() { //... } return {

publicMethod

}

}

If you prefer, you can use classes (or whatever is your personal preference) to have private and public methods. You could even have everything public.

Conclusion

With this approach you can have portable Three.js code that can work with any framework or without a framework. You can keep working with Three.js in the way you are used to with regular HTML pages and, at the same time, enjoy the clarity and structure that React gives to your web applications.