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

如何解决React.StrictMode导致useEffect执行两次的问题

解决方案

React 18 开发环境下StrictMode会双重执行挂载/卸载流程的副作用,用来检测未正确清理的逻辑。你当前代码只做了初始化,没有加实例销毁逻辑和异步竞态处理,所以会触发两次Phaser实例创建。

标准兼容实现(推荐)

不要用网上流传的仅靠单个ref标记跳过执行的方案,这类方案没处理动态import的异步竞态问题,很容易失效。正确实现需要同时做执行锁、异步取消、实例销毁三步:

// 原有Phaser实例ref保留
const game = useRef(null);
// 新增跨渲染周期的初始化标记
const hasInitialized = useRef(false);

const query = useCallback(async () => {
  setLoading(true);
  try {
    console.log("Starting game in async")
    window.Phaser = await import('phaser')
    const Boot = await import('./bankPanicScenes/Boot.js')
    const Preloader = await import( './bankPanicScenes/Preloader.js')
    const MainMenu = await import( './bankPanicScenes/MainMenu.js')
    const MainGame = await import( './bankPanicScenes/Game.js')

    const config = {
      type: Phaser.AUTO,
      gameTitle: "Bank Panic",
      parent: "game-bankpanic",
      autoCenter: Phaser.Scale.CENTER_HORIZONTALLY,
      autoFocus: true,
      fps: {
        target: 60,
      },
      physics: {
        default: "arcade",
        arcade: {
          gravity: { y: 200 },
          debug: false,
        },
      },
      backgroundColor: "#282c34",
      scale: {
        mode: Phaser.Scale.ScaleModes.NONE,
      },
      scene: [Boot, Preloader, MainMenu, MainGame],
    }
    // 二次校验,避免重复创建
    if (hasInitialized.current || game.current) return;
    game.current = new Phaser.Game(config)
    hasInitialized.current = true;
    console.log("Game loaded")
  } catch (e) {
    console.log(e)
  } finally {
    setLoading(false);
    console.log('Loading Finished')
  }
}, [])

useEffect(() => {
  if (hasInitialized.current) return;
  // 异步取消标记,解决import加载期间effect被清理的竞态问题
  let isCancelled = false;

  const initGame = async () => {
    if (isCancelled) return;
    await query();
  };

  initGame();

  // 副作用清理函数,StrictMode触发模拟卸载、组件真实卸载时都会执行
  return () => {
    isCancelled = true;
    // 销毁已生成的Phaser实例,同时清理画布、事件、场景
    if (game.current) {
      game.current.destroy(true);
      game.current = null;
    }
    hasInitialized.current = false;
  };
}, [query]);

关键说明

  • 很多社区方案失效的核心原因是没处理异步逻辑的竞态:第一次effect触发的动态import还没执行完成时,StrictMode已经触发了第一次effect的清理流程,等异步资源加载完成后还是会创建第一个无效实例,最终出现两个游戏画布。
  • 调用game.current.destroy(true)时必须传true参数,才能让Phaser完整清理关联的DOM节点、全局事件监听、场景内存引用,避免DOM重复挂载报错和内存泄漏。
  • 不推荐直接移除<React.StrictMode>的方案,StrictMode可以提前暴露绝大多数副作用类bug,上面的实现完全符合React官方的副作用编写规范,开发、生产环境均可稳定运行。
  • 不要把Phaser初始化逻辑放到组件作用域外或者useMemo里规避双执行:前者在SSR框架(Next.js、Remix等)中会因为服务端不存在window对象直接报错,后者在React未来版本中不保证只执行一次。

内容的提问来源于stack exchange,提问作者Noah B

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 16:48:44