React Hooks + 3JS
Introduction — What is React Hooks? What is 3JS?
Many people who build websites or mobile apps know about a javascript framework called React. React helps developers build components for their basic elements of HTML, CSS, and Javascript. As of version 16.8, React received an update to add a different way of writing these components in a functional way, called React Hooks. Below is an example of both types, class and Hook components.
//React Class component
class HelloMessage extends React.Component {
render() {
return (
<div>
Hello {this.props.name}
</div>
);
}
}//React Hook component
const HelloMessage = (props) =>{
return (
<div>
Hello {props.name}
</div>
);
}
I won’t go in to deep on the differences between these two in this tutorial, but all we need to know are:
- Hooks does not have lifecycle functions like componentDidMount or componentDidUpdate, instead they have a useEffect() function.
- To define a state in Hooks, you would use a useState() function instead of a constructor
Another piece of technology I would like to introduce is 3JS. 3JS is a JavaScript 3D library that uses webGL to render objects onto a webpage. With 3JS, you can define scenes, cameras, and 3D objects and create awesome transitions or animations.
Why React Hooks + 3JS?
You might be wondering at this point, what is the purpose of bringing these two random technologies together. Normally, you would use vanilla HTML to place your objects in the DOM, use CSS for the transitions and use Javascript to hook up actionable items. However, with 3JS we can create objects and use a plethora of transition effects prebuilt into the 3JS library. We then can use React hooks to put these objects into a component, so whenever we need to create multiples of the object we can just call on the component. In the long run, being able to templatize your objects can save you a lot of time and space.
Lets get right into it! In this tutorial, we will be making colorful cubes that will change its location and scaling size.
Before we do anything, you will need to create your react project. An easy way to create a react app is to use create-react app.
npx create-react-app my-app
Scene, Camera, Render!
Before we add our threeJS items, we need to make sure we are importing from the three JS library.
import React, {useEffect, useState} from 'react'
import * as THREE from "three";
Using THREE from three, we can now initializing a scene, camera, and renderer in the useEffect() function.
useEffect(()=>{ //Scene, camera, and renderer
let scene = new THREE.Scene();
scene.background = new THREE.Color( 0xffffff ); let camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 ); let renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight ); document.body.appendChild(renderer.domElement) setPages({scene, camera, renderer}) camera.position.z = 60;},[])
What can we find in our useEffect?
- We have a scene that is assign a background color of white.
- We have a Perspective Camera with a field of view of 75, the aspect ratio to be the window.innerWidth/window.innerHeight, near plane of 0.1, and far plane of 1000.
- We also have a renderer that uses WebGL and sets the size to be the window’s inner width and height.
Now that we defined our basic canvas elements we need to apply them to the actual DOM. Renderer.domElement provides an output that can be appended to the document.body.
The settings really depends on how you want to define your canvas and how you want the user to perceive the 3D objects. You can change each element with the built-in methods. In my example above, I added a different position to the camera by defining the z axis to 60.
For more references on scenes, cameras and renderers, please read the following documentation.
In the next, section I will talk about why I added these three elements into an state through setPages.
Cube Creation Setup
With the scene, camera, and renderer, we can now make any thing happen. For simplicity of this tutorial, we will try to make a rotating cube that appears in random locations and changes colors.
In order to get the full use of React components we want to separate the cube creation in a new component, but before we do that let us setup the variables we need to pass.
let [page, setPages] = useState({})
let [cube, setCube] = useState([{name:”initCube”}])
let [count, setCount] = useState(1)
As we mentioned above, we need to use the useState to create a local state that holds our cube, scene, and render, so we can pass it to our separate cube component.
- page is a object that contains {scene, camera, renderer}
- cube is an array of objects that will hold the individual cubes
- count is a unique number identification for each cube
Now that we got our variables defined, we need to add cube objects into our array so we can map it. There are many ways to doing this, but the simplest is to create a function that pushes an object with a title (in our case it is the count incremented) and set it back to the cube array with setCube().
//Optional way of adding a cubeconst addCube=()=>{
cube.push({count})
setCount(count+1)
setCube(cube)
}
Our return value will look like this:
return(
<>
{isLoaded ?
cube.map((count, index)=><Cube page={page} key={index}> </Cube>)
:null} <Button onClick={addCube}>Click to add Cube</Button>
</>
)
A simple .map through our cube array will iterate through and create a cube for each instance in the array. We will then pass the page state, which is holding our scene, camera and renderer, and our index (a unique key id).
So now whenever the button is clicked a new cube is generated in our array, and React will re-render the page with a new cube because the state of cube array has been updated.
What about the scene, camera, and renderer? Good question! They do not get re-rendered because we added a dependancies array as a second parameter. A dependancies array holds all the states that the specific useEffect relies on, so when a re-render happens the callback function will run again. In our case, we do not want to re-render the scene, camera, or renderer again, so we do not put any dependencies in the array.
Cube.js
Now that we have everything set up in our parent file, lets create a cube.js file that will be the skeleton structure or template of our cube object.
useEffect(()=>{
// Deconstruct page prop
let {scene, camera, renderer} = props.page; // Define materials and shape
let geometry = new THREE.BoxGeometry( 1, 1, 1 );
let material = new THREE.MeshBasicMaterial( { color: 0x000000 } );
// Create cube and add to scene
let cube = new THREE.Mesh( geometry, material );
scene.add( cube ); // Define cube's and location's max and min
let max= 10;
let min= 0; let cMin = -35
let cMax = 35 //Set increase value to 0.05
let value = 0.05 //Set initial cube position
cube.position.set(
Math.floor(Math.random() * (cMax - cMin + 1) ) + cMin,
Math.floor(Math.random() * (cMax - cMin + 1) ) + cMin,
0) //Define a randomPosition function that sets a new location for the cube
let randomPosition=()=>{
let pos = {
x:Math.floor(Math.random() * (cMax - cMin + 1) ) + cMin,
y:Math.floor(Math.random() * (cMax - cMin + 1) ) + cMin,
z:0,}
cube.position.set(pos.x,pos.y,pos.z)
}},[])
There is a lot going on in this useEffect() function.
- We are destructuring the props.pages to isolate the scene, camera, and renderer.
- Defining our materials, shape and color of the object with BoxGeometry and MeshBasicMaterial
- Creating the cube with Mesh
- Adding the cube to the parent scene
- Defining our max and min range for our cube position and location boundary
- Set value variable for the cubes increase rate
- Set the initial cube position.
- Define a randomPosition function that changes a cubes position in a random location.
The purpose of all these steps is to initialize your cube parameters and allow it to changes it’s locations randomly.
There are plenty more materials and different shapes to play around with. Checkout the materials section in the threejs material section. I also found the BoxGeometry and MeshBasicMaterial sections to be very useful as well.
Now that we have our cube constructed, we can now start animating it.
Animation
Within the same useEffect function that we initialized our cube, we can place a animate function. This animate function will be called repetitively, to increase a specific aspect of the cube.
In the example below, we are increasing the rotation by 0.08 and increasing the cube’s scaling by the value we defined above.
//allow for cube to scale up
let scaleUp = true;//Animate function
let animate = function () {
//call the animate function repetitively
requestAnimationFrame( animate );
//Add a rotation
cube.rotation.x += 0.08;
cube.rotation.y += 0.08; //if the cube's scale reaches a 0 mod
//then change its color
if(cube.scale.x % 2 < 0 ){
r = Math.random(0,250)
b = Math.random(0,250)
g = Math.random(0,250)
randomPosition();
}
//
cube.material.color.setRGB(r,g,b) //Scale the cube up to its max and min
if(cube.scale.x > max){
scaleUp = false;
}else if(cube.scale.x < min){
scaleUp = true;
}
if(scaleUp){
cube.scale.y += value;
cube.scale.x += value;
cube.scale.z += value;
}else{
cube.scale.y -= value;
cube.scale.x -= value;
cube.scale.z -= value;
}
//re-render the scene and camera again with new specs
renderer.render( scene, camera );
};//initial call on the animate function
animate();
In this animation function, you can put anything to manipulate the cube’s appearance, shape, or transitions. However, if you don’t want to have this animation on a repetition you can associate a clickable object on the screen to run an animation or set a timer to start an animation. There are plenty of ways to do this. The fun part is finding new ways to alter the objects on the screen and you can be very creative with it.
Conclusion
Thank you for following through this tutorial and I hope this helped you with implementing 3JS into your react projects. I highly recommend reading the documentation on the official 3JS site because there is a lot of material that this tutorial didn’t cover. Also, there are plenty of example projects that use 3JS, which can spark some inspiration. Feel free to share your creative projects in the comment section below or contact me if you have any questions.