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
相关产品推荐
相关产品推荐

