Bringing 3D to Life: Integrating Three.js with React, Redux and MUI
Tags: react, threejs, javascript, webgl
Published: 2023-10-30 07:46:53

The web is not a flat canvas. So why limit ourselves to two dimensions? With the rise of virtual reality and augmented reality, the future of web design is clear: it’s three-dimensional. This article will explore the synergy between Three.js, React, Redux for state management, and Material-UI (MUI) for polished interfaces, giving you hands-on examples straight from [my GitHub repository](https://github.com/chamallakshika09/react-threejs).

## Prerequisites
Before we dive deep, you should have a basic understanding of JavaScript, React, and Redux. Familiarity with Three.js and MUI is beneficial but not mandatory. Ensure your development environment includes Node.js, npm or yarn, and create-react-app.

## Setting Up Your Environment
Let’s set up a React project, then add Three.js, Redux, and MUI:

```
npx create-react-app my-threejs-project
cd my-threejs-project
npm install three react-redux @reduxjs/toolkit @mui/material @mui/icons-material @emotion/react @emotion/styled
```

## Understanding Three.js Basics
Three.js is the gatekeeper to 3D rendering in the web browser, utilizing WebGL’s power. Its components, like scenes, cameras, renderers, geometries, materials, and lights, are the building blocks for our 3D world. We’ll demystify these terms and provide you with a roadmap for navigating the Three.js landscape.

## Designing with MUI and Integrating Redux
MUI provides a robust set of components and styles for React applications, making our UI sleek and responsive. Redux, on the other hand, offers a structured approach to manage application state.

**Setting up Redux**: To integrate Redux, you’ll need to define actions, reducers, and then integrate the store with your application:

```js
import { configureStore } from '@reduxjs/toolkit';
import cubeSlice from './cube-slice';

export const store = configureStore({
  reducer: {
    cube: cubeSlice,
  },
});
```
Wrap your application with the Redux `Provider`:

```js
import { Provider } from 'react-redux';
import store from './store';

function App() {
  return (
    <Provider store={store}>
      {/* ... rest of your components */}
    </Provider>
  );
}
```
**Integrating MUI with Three.js Scene**: For interaction controls like color changing, you can use MUI’s Button component:

```js
import Button from '@mui/material/Button';

// ... within your component
<Button variant="contained" color="primary" onClick={() => setColor(0xff0000)}>Change Color</Button>
```

## Creating Your First React Component with Three.js
Now, we roll up our sleeves and dive into the code. Our mission: create a React component that houses our 3D scene. Instead of mixing our UI logic and 3D graphics logic, we’ll keep things clean by isolating the Three.js code in a separate file. This separation of concerns not only makes the code more manageable but also enhances reusability.

First, let’s create a new file to hold our Three.js logic, which we’ll call `three-setup.js`.

```js
import * as THREE from 'three';

export const createScene = () => new THREE.Scene();

export const createCamera = () => {
  const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.z = 5;
  return camera;
};

export const createRenderer = (mount) => {
  const renderer = new THREE.WebGLRenderer();
  renderer.setSize(window.innerWidth, window.innerHeight);
  mount.appendChild(renderer.domElement);
  return renderer;
};

export const createCube = () => {
  const geometry = new THREE.BoxGeometry();
  const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
  return new THREE.Mesh(geometry, material);
};

export const animate = (renderer, scene, camera, cube) => {
  function animation() {
    requestAnimationFrame(animation);

    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

    renderer.render(scene, camera);
  }
  animation();
};
```
In `three-setup.js`, we've defined functions to set up the scene, camera, renderer, and cube, and to animate our object. Each function is responsible for one specific aspect of the 3D scene, making the code modular and easier to maintain or update.

Now, let’s use these functions in our main React component:

```js
import React, { useRef, useEffect } from 'react';
import { createScene, createCamera, createRenderer, createCube, animate } from './three-setup';

function ThreeScene() {
  const mountRef = useRef(null);
  const cubeRef = useRef(null);

  useEffect(() => {
    const scene = createScene();
    const camera = createCamera();
    const renderer = createRenderer(mountRef.current);
    const cube = createCube();
    cubeRef.current = cube;

    scene.add(cube);

    animate(renderer, scene, camera, cube);

    return () => {
      mountRef.current.removeChild(renderer.domElement);
    };
  }, []);

  return <div ref={mountRef} />;
}

export default ThreeScene;
```
In `three-scene.js`, we're importing our setup functions from three-setup.js. This way, the React component stays clean and focused on the component lifecycle, while the 3D setup and logic are abstracted away.

This separation is a common best practice in software development, known as the separation of concerns (SoC). It allows for better readability, easier maintenance, and scalability, especially when your project grows in complexity.

## Managing Resources and Cleanups
In the world of 3D web graphics, performance is paramount. Efficient resource management — especially cleaning up and cancelling ongoing processes — is crucial when your components unmount. In React, this is typically done in the `useEffect` cleanup function, ensuring we're being good web citizens by cleaning up after ourselves to avoid memory leaks.

Let’s update our `ThreeScene` component to cancel the animation frame when the component unmounts.

First, we need to modify our `animate` function in `three-setup.js` to return the animation frame ID, which is returned by `requestAnimationFrame`.

```js
export const animate = (renderer, scene, camera, cube) => {
  function animation() {
    const frameId = requestAnimationFrame(animation);

    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

    renderer.render(scene, camera);

    return frameId; // We're returning the frame ID here
  }
  return animation(); // And initiating the animation here
};
```
Now, in our `three-scene.js`, we'll store this animation frame ID and use it to cancel the animation on component unmount.

```js
import React, { useRef, useEffect } from 'react';
import { createScene, createCamera, createRenderer, createCube, animate } from './three-setup';

function ThreeScene() {
  const mountRef = useRef(null);

  useEffect(() => {
    const scene = createScene();
    const camera = createCamera();
    const renderer = createRenderer(mountRef.current);
    const cube = createCube();
    cubeRef.current = cube;

    scene.add(cube);

    const frameId = animate(renderer, scene, camera, cube); // Storing the frame ID

    return () => {
      mountRef.current.removeChild(renderer.domElement);
      cancelAnimationFrame(frameId); // Cancelling the animation frame here
    };
  }, []);

  return <div ref={mountRef} />;
}

export default ThreeScene;
```
With the `cancelAnimationFrame` function, we tell the browser to stop the ongoing animation process associated with the provided frame ID. This is crucial for preventing potential memory leaks, ensuring that the JavaScript heap size doesn't grow indefinitely due to abandoned objects and closures from the obsolete component instance.

## Managing State, Interactivity, and Redux Integration
What’s a 3D object if it can’t interact with your users? A static entity. We’ll breathe life into our 3D objects by managing component state and handling user interactions like clicks, drags, and more.

Add a Redux action to manage color changes:

```js
import { createSlice } from '@reduxjs/toolkit';

export const cubeSlice = createSlice({
  name: 'cube',
  initialState: {
    color: 0x00ff00,
  },
  reducers: {
    setColor: (state, action) => {
      state.color = action.payload;
    }
  },
});

export const { setColor } = cubeSlice.actions;
export default cubeSlice.reducer;

export const selectColor = (state) => state.cube.color;
```
Update your component to dispatch this action:

```js
import { useDispatch } from 'react-redux';
import { setColor } from './cube-slice';

function ThreeScene() {
  const dispatch = useDispatch();
  const color = useSelector(selectColor);
  // ... other component logic

  useEffect(() => {
    if (cubeRef.current) {
      cubeRef.current.material.color.set(color);
    }
  }, [color]);

  return (
    <>
      <div ref={mountRef} />
      <Button onClick={() => dispatch(setColor(0xff0000))}>Change Color</Button>
    </>
  );
}
```
With this code, we introduce interactivity, allowing users to engage with our 3D object in real time. To see the complete implementation, check out the corresponding code in [my GitHub repository](https://github.com/chamallakshika09/react-threejs).

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wpna80mvaq2h6ty32hfd.PNG)

## Conclusion
Merging Three.js with React might seem daunting initially, but the rewards are well worth the effort. The blend of Three.js’s graphical capabilities with React’s efficient rendering opens up a universe of possibilities for web developers and UI/UX designers. So embark on this journey, experiment, learn, and most importantly, have fun creating something magical!

If you want to delve deeper and explore the codebase, feel free to browse [the project on GitHub](https://github.com/chamallakshika09/react-threejs). Embark on this journey, experiment, learn, and most importantly, have fun creating something magical!

## Further Resources

For those who want to explore deeper waters, check out the [Three.js Documentation](https://threejs.org/docs/), [React Documentation](https://react.dev/learn), [Redux Documentation](https://redux.js.org/introduction/getting-started), [MUI Documentation](https://mui.com/material-ui/getting-started/), [react-three-fiber on GitHub](https://github.com/pmndrs/react-three-fiber) and [the complete code for this tutorial on my GitHub](https://github.com/chamallakshika09/react-threejs).