Calling useRef inside a callback

Viewed 1681

So I've been out of react since before hooks and haven't used threeJS before, but I'm trying to hit 2 birds with one stone so excuse me if it's a rookie mistake.

what I'm trying to do is render a Three.js scene inside a react document body, how I'm trying to do that is through running the three.js code in useEffect() and setting a reference to my react document using useRef(), however, apparently useEffect runs before the document is rendered, hence breaking, so I tried using a ref callback like this

import { useRef, useEffect, useCallback } from "react";
// Packages
import * as THREE from "three";
// Styling
import "./homePage.scss";

function HomePage() {
  // Declare a new mounting reference
  const mountRef = useCallback((node) => {
    if (node !== null) {
      useRef(null);
    }
  }, []);
  // Lifecycle hook
  useEffect(() => {
    console.log(mountRef);
    // === THREE.JS CODE START ===
    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);
    // use ref as a mount point of the Three.js scene instead of the document.body
    mountRef.appendChild(renderer.domElement);
    var geometry = new THREE.BoxGeometry(1, 1, 1);
    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();
    // === THREE.JS CODE END ===
  }, []);

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

export default HomePage;

However, now I have the following error React Hook "useRef" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook

How can I fix this situation? Thanks!

2 Answers

You use useRef at the top level of your component, not in a callback (don't use useCallback for it):

const mountRef = useRef(null);

Then in your useEffect, declare mountRef.current as a dependency and only use it if it exists, see *** comments:

useEffect(() => {
    // *** If we don't have the DOM element yet, wait for it
    if (!mountRef.current) {
        return;
    }
    // === THREE.JS CODE START ===
    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);
    // use ref as a mount point of the Three.js scene instead of the document.body
    mountRef.current.appendChild(renderer.domElement);
    //      ^^^^^^^^−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− *** Use the DOM element
    var geometry = new THREE.BoxGeometry(1, 1, 1);
    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();
    // === THREE.JS CODE END ===
}, []);

You probably want to include a cleanup callback (a function you return from useEffect) to remove the Three DOM element from the div on unmount:

useEffect(() => {
    // ***
    const { current } = mountRef;
    if (!current) {
        return;
    }
    // === THREE.JS CODE START ===
    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);
    // use ref as a mount point of the Three.js scene instead of the document.body
    // ***
    const {domElement} = renderer;
    current.appendChild(domElement);
    var geometry = new THREE.BoxGeometry(1, 1, 1);
    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();
    // === THREE.JS CODE END ===
    // ***
    return () => {
        current.removeChild(domElement);
    };
}, []);

Note there how I grabbed mountRef.current and renderer.domElement to local constants. That makes the cleanup callback more reliable, since those properties can be changed outside the context of the useEffect callback.

Live Example:

const {useRef, useEffect} = React;

function HomePage() {
    // Declare a new mounting reference
    const mountRef = useRef(null);
    // Lifecycle hook
    useEffect(() => {
        const { current } = mountRef;
        console.log("current", current);
        // If we don't have the DOM element yet, wait for it
        if (!current) {
            return;
        }
        // === THREE.JS CODE START ===
        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);
        // use ref's DOM ELEMENT as a mount point of the Three.js scene instead of the document.body
        const { domElement } = renderer;
        current.appendChild(renderer.domElement);
        var geometry = new THREE.BoxGeometry(1, 1, 1);
        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();
        // === THREE.JS CODE END ===
        // Cleanup callback on component unmount
        return () => {
          current.removeChild(domElement);
        };
    }, []);
   
    return <div ref={mountRef} />;
}

ReactDOM.render(
    <HomePage/>,
    document.getElementById("root")
);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r124/three.min.js"></script>

You can't define hooks in callbacks, see Rules of Hooks.

To get the div ref, you only need to provide the reference from useRef, no useCallback needed at all, see docs examples.

const mountRef = useRef(null);
<div ref={mountRef} />
Related