You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React集成ThreeJS添加场景对象时遇无效Hook调用错误求助

问题:React中使用ThreeJS触发"Invalid hook call"错误

首次在React中使用ThreeJS,尝试创建带有定时更新功能的Actor组件并将对象添加到场景时,触发错误:

Invalid hook call. Hooks can only be called inside of the body of a function component.

相关组件代码如下:

import React, { useRef, useEffect } from "react";
import { useFrame, useThree } from "react-three-fiber";
import * as THREE from "three";

const Actor = () => {
  const meshRef = useRef();
  const { scene } = useThree();

  useEffect(() => {
    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
    const mesh = new THREE.Mesh(geometry, material);
    mesh.rotateY(Math.PI);

    // Set initial properties and functions
    mesh.instructions = [];
    mesh.target = new THREE.Object3D().copy(mesh, false);
    mesh.targetRadiansOnY = 0;
    mesh.currentRadiansOnY = 0;
    mesh.mass = 0.1;
    mesh.velocity = new THREE.Vector3();
    mesh.angularVelocity = 0.015;
    mesh.topSpeed = 0.05;
    mesh.topAccelleration = 0.0015;
    mesh.accelleration = new THREE.Vector3();
    mesh.currentInstruction = null;
    mesh.gravityForce = new THREE.Vector3(0.0, -0.01, 0.0);

    // Add mesh to the scene
    scene.add(mesh);
    meshRef.current = mesh;

    return () => {
      // Clean up the mesh when the component unmounts
      scene.remove(mesh);
    };
  }, [scene]);

  useEffect(() => {
    // Update function to consume commands
    const consumeCommands = () => {
      const mesh = meshRef.current;
      if (mesh.currentInstruction) {
        let instruction = mesh.currentInstruction;
        let movementType = instruction["type"];
        let movementValue = instruction["value"];
        let dir = null;
        switch (movementType) {
          case "move_forward":
            dir = new THREE.Vector3().subVectors(mesh.target.position, mesh.position);
            dir.setLength(mesh.topAccelleration);
            mesh.applyForce(dir);
            mesh.applyForce(mesh.gravityForce);
            mesh.velocity.add(mesh.accelleration);
            mesh._limitVelocity(mesh.topSpeed);
            mesh.position.add(mesh.velocity);
            mesh._limitGravity();
            mesh.accelleration.multiplyScalar(0.0);
            break;
          case "jump_forward":
            dir = new THREE.Vector3().subVectors(mesh.target.position, mesh.position);
            let upForce = new THREE.Vector3(0, (0.012 * (dir.length() / movementValue)), 0);
            dir.setLength(mesh.topAccelleration);
            mesh.applyForce(dir);
            mesh.applyForce(upForce);
            mesh.applyForce(mesh.gravityForce);
            mesh.velocity.add(mesh.accelleration);
            mesh._limitVelocity(mesh.topSpeed);
            mesh.position.add(mesh.velocity);
            mesh._limitGravity();
            mesh.accelleration.multiplyScalar(0.0);
            break;
          case "turn":
            if (movementValue === "turnLeft") {
              mesh.rotateY(mesh.angularVelocity);
              mesh.currentRadiansOnY += mesh.angularVelocity;
            } else {
              mesh.rotateY(-mesh.angularVelocity);
              mesh.currentRadiansOnY += mesh.angularVelocity;
            }
            break;
          default:
            console.log("command not implemented");
            break;
        }
        if (mesh._targetReached(movementType)) {
          mesh._nextAnimation();
        }
      }
    };

    // Set the consumeCommands function as an update function
    useFrame(() => {
      consumeCommands();
    });
  }, []);

  return null; // We don't render anything for this component
};

export default Actor;

错误原因

你把useFrame这个Hook放在了useEffect的回调函数内部,违反了React Hook的核心规则:所有Hook必须直接在函数组件的顶层作用域调用,不能嵌套在循环、条件或其他函数(包括useEffect的回调)里面。

解决方案

直接把useFrame移到组件的顶层作用域,不需要包裹在useEffect里。useFrame本身就是react-three-fiber提供的用于帧更新的Hook,它会自动在每帧执行回调,而且内部已经处理了依赖和清理逻辑。

修改后的代码如下:

import React, { useRef, useEffect } from "react";
import { useFrame, useThree } from "react-three-fiber";
import * as THREE from "three";

const Actor = () => {
  const meshRef = useRef();
  const { scene } = useThree();

  useEffect(() => {
    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
    const mesh = new THREE.Mesh(geometry, material);
    mesh.rotateY(Math.PI);

    // Set initial properties and functions
    mesh.instructions = [];
    mesh.target = new THREE.Object3D().copy(mesh, false);
    mesh.targetRadiansOnY = 0;
    mesh.currentRadiansOnY = 0;
    mesh.mass = 0.1;
    mesh.velocity = new THREE.Vector3();
    mesh.angularVelocity = 0.015;
    mesh.topSpeed = 0.05;
    mesh.topAccelleration = 0.0015;
    mesh.accelleration = new THREE.Vector3();
    mesh.currentInstruction = null;
    mesh.gravityForce = new THREE.Vector3(0.0, -0.01, 0.0);

    // 补充代码中缺失的方法(否则运行会报错)
    mesh._limitVelocity = function(topSpeed) {
      if (this.velocity.length() > topSpeed) {
        this.velocity.setLength(topSpeed);
      }
    };

    mesh._limitGravity = function() {
      // 基础地面检测逻辑:限制y轴位置不低于0
      if (this.position.y < 0) {
        this.position.y = 0;
        this.velocity.y = 0;
      }
    };

    mesh._targetReached = function(movementType) {
      // 根据运动类型判断是否到达目标的基础逻辑
      if (movementType === "move_forward" || movementType === "jump_forward") {
        return this.position.distanceTo(this.target.position) < 0.01;
      } else if (movementType === "turn") {
        return Math.abs(this.currentRadiansOnY - this.targetRadiansOnY) < 0.01;
      }
      return false;
    };

    mesh._nextAnimation = function() {
      // 切换到下一个指令的逻辑
      if (this.instructions.length > 0) {
        this.currentInstruction = this.instructions.shift();
      } else {
        this.currentInstruction = null;
      }
    };

    // Add mesh to the scene
    scene.add(mesh);
    meshRef.current = mesh;

    return () => {
      // Clean up the mesh when the component unmounts
      scene.remove(mesh);
    };
  }, [scene]);

  // 直接在组件顶层使用useFrame
  useFrame(() => {
    const mesh = meshRef.current;
    if (!mesh || !mesh.currentInstruction) return;

    let instruction = mesh.currentInstruction;
    let movementType = instruction["type"];
    let movementValue = instruction["value"];
    let dir = null;
    switch (movementType) {
      case "move_forward":
        dir = new THREE.Vector3().subVectors(mesh.target.position, mesh.position);
        dir.setLength(mesh.topAccelleration);
        mesh.applyForce(dir);
        mesh.applyForce(mesh.gravityForce);
        mesh.velocity.add(mesh.accelleration);
        mesh._limitVelocity(mesh.topSpeed);
        mesh.position.add(mesh.velocity);
        mesh._limitGravity();
        mesh.accelleration.multiplyScalar(0.0);
        break;
      case "jump_forward":
        dir = new THREE.Vector3().subVectors(mesh.target.position, mesh.position);
        let upForce = new THREE.Vector3(0, (0.012 * (dir.length() / movementValue)), 0);
        dir.setLength(mesh.topAccelleration);
        mesh.applyForce(dir);
        mesh.applyForce(upForce);
        mesh.applyForce(mesh.gravityForce);
        mesh.velocity.add(mesh.accelleration);
        mesh._limitVelocity(mesh.topSpeed);
        mesh.position.add(mesh.velocity);
        mesh._limitGravity();
        mesh.accelleration.multiplyScalar(0.0);
        break;
      case "turn":
        if (movementValue === "turnLeft") {
          mesh.rotateY(mesh.angularVelocity);
          mesh.currentRadiansOnY += mesh.angularVelocity;
        } else {
          mesh.rotateY(-mesh.angularVelocity);
          mesh.currentRadiansOnY -= mesh.angularVelocity; // 修正原代码的角度计算错误
        }
        break;
      default:
        console.log("command not implemented");
        break;
    }
    if (mesh._targetReached(movementType)) {
      mesh._nextAnimation();
    }
  });

  return null; // We don't render anything for this component
};

export default Actor;

另外补充两点注意事项:

  • 原代码中mesh._limitVelocity、mesh._limitGravity等方法未定义,运行时会触发报错,因此在修改后的代码中补充了这些方法的基础实现。
  • 原代码中turn分支的角度更新逻辑存在错误,右转时应该减去mesh.angularVelocity,否则左右转的角度计算会出现偏差。

内容的提问来源于stack exchange,提问作者Michael Water

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 16:35:18