【问题标题】:Calling useRef inside a callback在回调中调用 useRef
【发布时间】:2026-02-02 08:50:01
【问题描述】:

所以我从钩子之前就没有反应,之前没有使用过threeJS,但我试图用一颗石头击中2只小鸟,如果这是一个菜鸟的错误,请原谅。

我要做的是在 react 文档正文中渲染 Three.js 场景,我尝试通过在 useEffect() 中运行 three.js 代码并设置对我的 react 的引用使用 useRef() 的文档,然而,显然 useEffect 在文档呈现之前运行,因此中断,所以我尝试使用这样的 ref 回调

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;

但是,现在我有以下错误 React Hook "useRef" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook

我该如何解决这种情况?谢谢!

【问题讨论】:

    标签: javascript reactjs react-hooks


    【解决方案1】:

    您在组件的顶层使用useRef,而不是在回调中(不要使用useCallback):

    const mountRef = useRef(null);
    

    然后在您的useEffect 中,将mountRef.current 声明为依赖项,并且仅在存在时才使用它,请参阅*** cmets:

    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 ===
    }, []);
    

    您可能希望包含一个清理回调(从 useEffect 返回的函数)以在卸载时从 div 中删除三个 DOM 元素:

    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);
        };
    }, []);
    

    请注意我是如何将mountRef.currentrenderer.domElement 抓取到本地常量的。这使得清理回调更加可靠,因为这些属性可以在 useEffect 回调的上下文之外更改。

    现场示例:

    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>

    【讨论】:

    • 我收到了关于您的依赖项的警告,我不太明白为什么需要,但删除它仍然可以正常工作
    • React Hook useEffect 有一个不必要的依赖:'mountRef.current'。排除它或删除依赖数组。像“mountRef.current”这样的可变值不是有效的依赖项,因为改变它们不会重新渲染组件
    • @OmarHussein - Doh! 不,不需要依赖,因为 ref 是无条件使用的,并且在 @987654321 之前不会调用 useEffect 回调@。很抱歉!
    • 不用担心 :D 感谢您的帮助和解释
    • @SaajidAkram - 您使用的是 TypeScript,这意味着您必须告诉 TS ref 将引用什么。通常,您使用useRef 上的类型参数 来执行此操作。例如,如果您要在 div 上使用 ref(如在这个问题中),那就是 HTMLDivElement,所以您将使用 const mountRef = useRef&lt;HTMLDivElement&gt;(null);mountRef.current 的类型将是 HTMLDivElement | null,因此您需要使用解构和保护(如第二个 useEffect 示例中所示),以便 TypeScript 可以缩小类型。
    【解决方案2】:

    您不能在回调中定义挂钩,请参阅Rules of Hooks

    要获得div ref,您只需要提供来自useRef 的引用,根本不需要useCallback,请参阅docs examples

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

    【讨论】:

    • 你也不能有条件地称呼它