基于Yarn workspace & lerna 的项目中 报错 Invalid hook call. Hooks can only be called inside of the body of a function component.
复现过程:
yarn start
http://localhost:3000/sub-app
报错信息:
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
- You might have mismatching versions of React and the renderer (such as React DOM)
- You might be breaking the Rules of Hooks
- You might have more than one copy of React in the same app
See https://fb.me/react-invalid-h... for tips about how to debug and fix this problem.
项目地址 bug-demo
项目结构
mono-repo
-packages
-container
-subApp
在container项目中引入subApp打包后的文件 路由/sub-app
下 当引入的是一个hooks组件就会报错 而路由到/sub-app/foo
由于是class组件所以没有问题
部分关键代码
// /packages/container/src/App.js
import React from 'react'
import { BrowserRouter, Switch, Route, Link } from 'react-router-dom';
import About from './About';
import Header from './Header';
import Loadable from 'react-loadable'
const Logo = () => {
return <div>LOGO</div>
}
const loadableComponent = (loader) => {
return Loadable({
loader: () =>
loader().then(
(res) => {
return res
},
(e) => () => {
console.log(e)
}
),
loading() {
return (
<div>loading</div>
)
}
})
}
const loadSubApp = (subAppInfo) => {
const { name, host } = subAppInfo
return new Promise((resolve, reject)=> {
fetch(`${host}/${name}/asset-manifest.json`)
.then(res => res.json())
.then(manifest => {
const script = document.createElement('script');
script.src = `${host}${manifest.files['main.js']}`;
const timeout = setTimeout(()=>{
console.error(`MicroApp ${name} timeout`);
reject(new Error(`MicroApp ${name} timeout`));
},20000)
script.onload = () => {
clearTimeout(timeout)
const app = window[name]
console.log({app, name})
console.log(`MicroApp ${name} loaded success`);
resolve(app)
}
script.onerror = (e) => {
clearTimeout(timeout);
console.error(`MicroApp ${name} loaded error`, e);
reject(e)
}
document.body.appendChild(script);
})
})
}
const subLoader = (name) => async () => {
const App = await loadSubApp({ name: 'subApp', host: process.env.REACT_APP_SUBAPP_HOST })
console.log({App, window})
return App[name]
}
const App = () => {
return (
<BrowserRouter>
<React.Fragment>
<Logo />
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/header">header</Link>
</li>
<li>
<Link to="/sub-app">subApp</Link>
</li>
<li>
<Link to="/sub-app/foo">subApp foo</Link>
</li>
</ul>
<Switch>
<Route exact path="/" component={About} />
<Route exact path="/header" render={() => <Header /> }/>
<Route exact path="/sub-app/foo" render={()=> {
const RenderSubApp = loadableComponent(subLoader('Foo'))
return <RenderSubApp />
}}/>
<Route exact path="/sub-app" render={()=> {
const RenderSubApp = loadableComponent(subLoader('Count'))
return <RenderSubApp />
}}/>
</Switch>
</React.Fragment>
</BrowserRouter>
)
}
export default App
// /packages/subApp/config-overrides.js
const {
override,
// addWebpackAlias
} = require("customize-cra");
// const path = require("path");
const dropConsole = () => {
return (config) => {
if (config.optimization.minimizer) {
config.optimization.minimizer.forEach((minimizer) => {
if (minimizer.constructor.name === "TerserPlugin") {
minimizer.options.terserOptions.compress.drop_console = true;
}
});
}
return config;
};
};
const optBuild= () => config => {
config.optimization.runtimeChunk = false;
config.optimization.splitChunks = {
cacheGroups: {
default: false,
},
};
return config
}
const disableSourceMap = () => (config) => {
if (process.env.NODE_ENV === "production") {
config.devtool = false;
}
return config;
};
const customizeCraOverride = override(
disableSourceMap(),
dropConsole(),
optBuild(),
// addWebpackAlias({
// 'react': path.resolve(__dirname, '../container/node_modules/react'),
// 'react-dom': path.resolve(__dirname, '../container/node_modules/react-dom')
// })
);
const webpack = (config, env) => {
const webpackConfig = customizeCraOverride(config, env);
return {
...webpackConfig,
output: {
...webpackConfig.output,
library: "subApp",
libraryTarget: 'window',
},
// externals: {
// 'react': 'react',
// 'react-dom': 'react-dom'
// },
};
};
module.exports = {
webpack,
};
如下这些方法都试过了 不起作用
- https://reactjs.org/warnings/invalid-hook-call-warning.html
- https://github.com/facebook/react/issues/13991
- https://github.com/facebook/react/issues/15315
- https://stackoverflow.com/questions/66488492/solve-having-more-than-one-copy-of-react-in-the-same-app
- https://stackoverflow.com/questions/61263658/yarn-workspaces-and-invalid-hook-call
在子应用的 package.json 文件里,设置 peerDependencies:
然后在子应用的 webpack 配置里:
然后在主应用里,要在加载子应用之前,把 React 和 ReactDOM 弄到全局作用域里:
子应用和主应用的版本react要一样