如何解决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
相关产品推荐
相关产品推荐

