Vite动态模块联邦加载多含React Hooks模块失败求助
Vite模块联邦加载多含Hooks的远程组件崩溃问题
我用Vite搭建了动态模块联邦测试项目,遇到一个问题:容器应用加载单个含React Hooks的远程组件时正常,加载多个就会崩溃,推测是竞态条件或React实例冲突导致的。
测试中,我在vapp1/src/components/AppHeader.tsx添加了简单的useState调用,直接引发崩溃,移除后恢复正常:
const [check, setCheck] = React.useState<boolean>(false);
核心代码展示
动态加载组件逻辑(container/src/components/RemoteComponent/index.tsx)
import React, { FC } from "react"; import ErrorBoundary from "../ErrorBoundary"; type Props = { fallback?: string | React.ReactNode; modulesToLoad: any[]; scope?: string; [key: string]: any; }; const loadComponent = (remoteUrl: string, moduleName: string) => async () => { const container = await import(remoteUrl); const factory = await container.get(moduleName); const Module = factory(); return Module; }; const RemoteComponent: FC<Props> = ({ modulesToLoad, scope = "default", fallback = null, ...props }) => { const [components, setCompoments] = React.useState<any>(); React.useEffect(() => { if (modulesToLoad.length > 0) { const promises = modulesToLoad.map( async (module: any) => await React.lazy(loadComponent(module.remoteUrl, `./${module.module}`)) ); Promise.all(promises).then((results) => { let tempComponents: any[] = []; results.forEach((Component) => { tempComponents.push( <ErrorBoundary> <React.Suspense fallback={fallback}>{<Component {...props} />}</React.Suspense> </ErrorBoundary> ); }); setCompoments(tempComponents); }); } }, [modulesToLoad]); return <> {components && components.map((component: any) => component)} </>; }; export default RemoteComponent;
容器应用加载触发逻辑(container/src/App.tsx)
import * as React from "react"; import "./App.css"; import useUserStore from "sbhContainer/UserStore"; import RemoteComponent from "./components/RemoteComponent"; function App() { const [user] = useUserStore(); const [showInfo, setShowInfo] = React.useState<boolean>(false); const [loadedModules, setLoadedModules] = React.useState<any[]>([]); const loadModules = (e: any) => { fetch("./externalRemotes.json").then((response) => { response.json().then((obj) => { setLoadedModules(obj); }); }); }; React.useEffect(() => { setShowInfo(true); }, [loadedModules]); return ( <div className="App"> <div> <h1>Container</h1> User in Container: {user} </div> <div style={{ marginTop: 10 }}> <button onClick={loadModules} style={{ backgroundColor: "#444" }}> Load Modules </button> {showInfo === true ? ( <> <RemoteComponent modulesToLoad={loadedModules} fallback={<div>Loading...</div>} /> </> ) : null} </div> </div> ); } export default App;
问题根源与修复方案
1. 错误使用React.lazy导致的状态异常
React.lazy本身接受一个返回Promise的函数,不需要用await包裹。你当前的代码把每个React.lazy的结果包装成了Promise,Promise.all拿到的是Promise对象而非懒加载组件,后续渲染自然出错。
修复代码:
// 原错误写法 const promises = modulesToLoad.map( async (module: any) => await React.lazy(loadComponent(module.remoteUrl, `./${module.module}`)) ); // 修改后直接生成懒加载组件 const lazyComponents = modulesToLoad.map( (module: any) => React.lazy(loadComponent(module.remoteUrl, `./${module.module}`)) );
2. 竞态条件防护
当modulesToLoad快速变化时,旧的异步加载可能未完成就触发新请求,导致状态被覆盖。用AbortController取消旧请求:
优化后的RemoteComponent:
const RemoteComponent: FC<Props> = ({ modulesToLoad, scope = "default", fallback = null, ...props }) => { const [components, setComponents] = React.useState<React.ReactNode[]>([]); React.useEffect(() => { const abortController = new AbortController(); const signal = abortController.signal; if (modulesToLoad.length > 0) { const tempComponents = modulesToLoad.map((module: any) => { const LazyComponent = React.lazy(loadComponent(module.remoteUrl, `./${module.module}`)); // 给每个组件加唯一key,避免React复用异常 return ( <ErrorBoundary key={`${module.remoteUrl}-${module.module}`}> <React.Suspense fallback={fallback}> <LazyComponent {...props} /> </React.Suspense> </ErrorBoundary> ); }); if (!signal.aborted) { setComponents(tempComponents); } } return () => abortController.abort(); // 卸载或依赖变化时取消请求 }, [modulesToLoad, props, fallback]); return <> {components} </>; };
3. 强制共享React实例(关键修复)
多模块加载崩溃最常见的原因是每个远程模块携带了自己的React实例,导致Hooks调用时出现"Invalid hook call"错误。
在所有远程模块的vite.config.ts中配置:
// 以vapp1为例 export default defineConfig({ plugins: [ react(), federation({ name: 'vapp1', filename: 'remoteEntry.js', exposes: { './AppHeader': './src/components/AppHeader.tsx' }, externals: ['react', 'react-dom'], // 标记为外部依赖 shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' }, }, }), ], });
容器应用的vite.config.ts同步配置:
export default defineConfig({ plugins: [ react(), federation({ name: 'container', remotes: {}, shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' }, }, }), ], });
内容的提问来源于stack exchange,提问作者bbasmer
相关产品推荐
相关产品推荐

