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

React函数组件热重载依赖未变时useEffect仍重复执行问题

核心原因

这个现象和React的依赖数组比对逻辑没有任何关系,是开发环境热模块替换(HMR,即热重载)的设计行为,不是代码bug。
热重载时构建工具会尽可能保留已有的运行时状态——这也是你观察到scene、camera的uuid始终不变、ref引用没有丢失的原因,但为了让你修改的代码逻辑及时生效,框架会重新执行组件模块代码、重跑所有effects,这个机制和依赖是否变化无关,哪怕你把依赖数组写成空数组、常量值,热重载时effect依然会触发。该行为仅存在于开发环境,生产环境构建后不存在这个问题。

解决方案

根据你的场景选以下方案即可,都是React标准写法,不会影响生产环境逻辑:

方案1:加effect清理逻辑(推荐,不影响热更新生效)

useEffect支持返回清理函数,热重载触发effect重跑前、组件卸载时都会先执行上一次effect的清理逻辑,在这里移除之前加载的模型、释放占用资源,就不会出现重复对象的问题:

useEffect(
  () => {
    console.log("CustomLog: Setting up scene");
    camera.position.z = 5.0;
    let loadedModel: Three.Object3D | null = null;
    
    const gltfLoader = new GLTFLoader();
    gltfLoader.load(
      "/Shoe.glb", 
      (shoeGLB) => {
        loadedModel = shoeGLB.scene;
        scene.add(shoeGLB.scene);
      }
    );

    // 清理逻辑:移除已加载模型、释放内存
    return () => {
      if (!loadedModel) return;
      scene.remove(loadedModel);
      loadedModel.traverse((obj) => {
        const mesh = obj as Three.Mesh;
        if (mesh.isMesh) {
          mesh.geometry.dispose();
          const material = mesh.material;
          Array.isArray(material) 
            ? material.forEach(m => m.dispose()) 
            : material.dispose();
        }
      })
    }
  },
  [scene, camera]
);

同时你需要修复动画循环的内存泄漏问题,现有代码每次热重载都会新增一个动画循环,长时间调试会导致页面卡顿:

// 组件内新增ref存动画id
const animateIdRef = useRef<number>(0);

const onContainerReferenced = useCallback(
  (container: HTMLDivElement) => {
    console.log(`CustomLog: Container element referenced`);
    if(container == null) {
      // 容器卸载时取消动画
      if (animateIdRef.current) cancelAnimationFrame(animateIdRef.current);
      return;
    }

    if(orbitControlsRef.current === undefined) orbitControlsRef.current = new OrbitControls(camera, container);
    const orbitControls: OrbitControls = orbitControlsRef.current;

    (camera as any).aspect = container.clientWidth / container.clientHeight;
    renderer.setSize(container.clientWidth, container.clientHeight);
    container.appendChild(renderer.domElement);
    
    orbitControls.domElement = container;
    orbitControls.update();

    function animate() {
      animateIdRef.current = requestAnimationFrame(animate);
      renderer.render(scene, camera);
    }
    animate();
  },
  []
);

方案2:加初始化标记(适合大模型等重初始化场景)

如果不想每次热重载都重新加载资源,可以用ref存初始化标记,已经执行过初始化就直接跳过逻辑:

// 组件内新增初始化标记ref
const isSceneInit = useRef(false);

useEffect(
  () => {
    if (isSceneInit.current) return;
    console.log("CustomLog: Setting up scene");
    camera.position.z = 5.0;
    
    const gltfLoader = new GLTFLoader();
    gltfLoader.load(
      "/Shoe.glb", 
      (shoeGLB) => {
        scene.add(shoeGLB.scene);
      }
    );
    isSceneInit.current = true;
  },
  [scene, camera]
);

注意:这个方案下如果修改了场景初始化相关代码,热重载不会同步变更,需要手动刷新页面才能看到效果。

额外提示

不要为了规避热重载行为去修改依赖数组、用全局变量存实例等黑科技绕过React生命周期,上述方案都是官方推荐的标准副作用处理方式,生产环境下HMR逻辑不会执行,effect只会在组件挂载时跑一次,完全符合你只初始化一次的需求。

内容的提问来源于stack exchange,提问作者Yamin Nather

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 01:54:28