Best way to integrate PLAIN Three.js inside a React app?

I’ve been wondering if there are other approaches about using Plain Three.js inside a React app (without using react-three or any other framework

I’ve seen many use cases such as putting the entire code inside an useEffect() or even something like this:

return (
  <>
    <script src="https://github.com/mrdoob/three.js/blob/03cc77fd1675c22d5765407ef5878a617cf6049d/build/three.min.js" />
    <script>
      // some threejs code
    </script>
  </>
)

And even making a seperate component for it.

But what is the best way in terms of performance to integrate plain Three.js inside a React App using FUNCTIONAL components? What is your approach?

2 Likes

This is the method I use.

  1. Create a React component with a reference to a <canvas> element.
  2. Initiate a JavaScript class, and pass the canvas reference to that class.
  3. Use the canvas inside the new class to create your WebGL renderer.

React component:

import React from 'react';
import ViewGL from './ViewGL';

export default class Scene extends React.Component {
    constructor(props) {
        super(props);
        this.canvasRef = React.createRef();
    }

    // ******************* COMPONENT LIFECYCLE ******************* //
    componentDidMount() {
        // Get canvas, pass to custom class
        const canvas = this.canvasRef.current;
        this.viewGL = new ViewGL(canvas);

        // Init any event listeners
        window.addEventListener('mousemove', this.mouseMove);
        window.addEventListener('resize', this.handleResize);
    }

    componentDidUpdate(prevProps, prevState) {
        // Pass updated props to 
        const newValue = this.props.whateverProperty;
        this.viewGL.updateValue(newValue);
    }

    componentWillUnmount() {
        // Remove any event listeners
        window.removeEventListener('mousemove', this.mouseMove);
        window.removeEventListener('resize', this.handleResize);
    }

    // ******************* EVENT LISTENERS ******************* //
    mouseMove = (event) => {
        this.viewGL.onMouseMove();
    }

    handleResize = () => {
        this.viewGL.onWindowResize(window.innerWidth, window.innerHeight);
    };

    render() {
        return (
            <div class="canvasContainer">
                <canvas ref={this.canvasRef} />
            </div>
        );
    }
}


Plain Three.js file

import * as THREE from 'three';

export default class ViewGL{
    constructor(canvasRef) {
        this.scene = new THREE.Scene();
        this.renderer = new THREE.WebGLRenderer({
            canvas: canvasRef,
            antialias: false,
        });

        // Create meshes, materials, etc.
        this.scene.add(myNewMesh);

        this.update();
    }

    // ******************* PUBLIC EVENTS ******************* //
    updateValue(value) {
      // Whatever you need to do with React props
    }

    onMouseMove() {
      // Mouse moves
    }

    onWindowResize(vpW, vpH) {
        this.renderer.setSize(vpW, vpH);
    }

    // ******************* RENDER LOOP ******************* //
    update(t) {
        this.renderer.render(this.scene, this.camera);

        requestAnimationFrame(this.update.bind(this));
    }
}

This way you can communicate between the React component and your plain Three.js file. React loves building & destroying components, so my only advice is to avoid this because WebGL and Three.js can consume a lot of memory if you re-build it multiple times. I prefer to just hide & show it after it’s been constructed, that way you avoid all WebGL re-building overhead. Only use componentWillUnmount if you’re 100% sure it won’t be re-built again.

Good luck!

3 Likes

What about doing it with a functional component? Would that be possible? Your code seems well optimized

1 Like

is there a reason why you’d do that w/o r3f? i see no benefit, all you’d do is create a split between declarative react and imperative three. r3f is not a wrapper or a binding, it just expresses three declaratively just like you express divs and spans via react-dom. imo what you’re doing is the same as making a component that suddenly builds part of the dom with queryselector and innerhtml.

1 Like

I actually use a hybrid setup. I generally have a basic r3f setup for ease of UI integration, and then simply use useFrame() when I need to use more “regular” 3js code. This works perfectly, and gives me the best of both worlds:

  • r3f allows me quick iterations and easy basic setup (usually have a Canvas/debug controls & helpers all done using r3f)
  • The useFrame() approach lets me do “regular” 3js stuff, preventing all my code being tied to both 3js and react.

Personally, this works for me quite well, but I guess it is more a matter of taste then anything else. Integrating 3js into react is quite complex if you want to do it right, and I’m happy to leave that to r3f :smiley:

1 Like

Deadlines. I can’t waste time to learn r3f.

1 Like

I made a little boilerplate here for that purpose (I tend to prefer the JS approach over the react-three-fiber approach as I can move a little quicker without the JSX abstraction).

you can see where the two integrate here: ts-react-threejs-boilerplate/index.tsx at 4467c6f0e1f98df90264c8f3c39d471cdd10eeab · scazan/ts-react-threejs-boilerplate · GitHub

I need to update a little bit for some better practices after learning some things but that is my general approach.

1 Like

Do you have any code examples of this setup?