There are three create-react-app
s customised using react-app-rewired
, both are using the same version of the following packages:
"react": "^16.12.0",
"react-app-rewired": "^2.1.5",
"react-dom": "^16.12.0",
"react-router-dom": "^5.1.2",
"react-scripts": "3.3.0",
App 1, the "Main" application is a very simple shell for the other two apps, public/index.html
kind of looks like so:
<body>
<div class="main-app"></div>
<div class="sub-app-1"></div>
<div class="sub-app-2"></div>
<script src="sub-app-1/static/js/bundle.js" async></script>
<script src="sub-app-1/static/js/0.chunk.js" async></script>
<script src="sub-app-1/static/js/main.chunk.js" async></script>
<script src="sub-app-2/static/js/bundle.js" async></script>
<script src="sub-app-2/static/js/0.chunk.js" async></script>
<script src="sub-app-2/static/js/main.chunk.js" async></script>
</body>
This works well enough, all three apps are rendered correctly. Now the requirements have changed slightly where the one and only component from sub-app-2
for example <PictureFrame />
needs to be included in sub-app-1
, I have created a stub component in the main project, like so:
const NullPictureFrame = () => {
return <div></div>;
}
export const PictureFrame = (props: Props) => {
const forceUpdate = useForceUpdate();
useEventListener(window, "PictureFrameComponent.Initialized" as string, () => forceUpdate());
const PictureFrame = window.PictureFrameComponent || NullPictureFrame;
return <PictureFrame />
}
I don't think the details of the hooks matter, but they do work when run in a stand alone fashion. The sub-app-2
does something similar to this
window.PictureFrameComponent = () => <PictureFrame />
which seems to work in theory, but in practice I end up with the following error
The above error occurred in the <PictureFrame> component:
in PictureFrame (at src/index.tsx:17)
in Router (at src/index.tsx:16)
in Unknown (at PictureFrames/index.tsx:21)
in PictureFrame (at src/index.tsx:32) <--- in `main-app`
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/docs/error-boundaries.html to learn more about error boundaries. index.js:1
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:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/warnings/invalid-hook-call-warning.html for tips about how to debug and fix this problem.
main-app
and sub-app-2
are loading two different versions of react
and this is causing an issue when using hooks.
I have tried to update my webpack config based on the advice in this github thread but it appears that sub-app-2
cannot find the reference to react
using this approach. My main-app/config-overrides.js
looks like so:
config.resolve.alias["react"] = path.resolve(__dirname, "./node_modules/react");
config.resolve.alias["react-dom"] = path.resolve(__dirname, "./node_modules/react-dom");
and my sub-app-1/config-overrides.js
looks like so:
config.externals = config.externals || {};
config.externals.react = {
root: "React",
commonjs2: "react",
commonjs: "react",
amd: "react"
};
config.externals["react-dom"] = {
root: "ReactDOM",
commonjs2: "react-dom",
commonjs: "react-dom",
amd: "react-dom"
};
Which results in no errors, but also the code from sub-app-2
not being initialised by webpack as react
/react-dom
cannot be found
Edit 1: Clarification
Window.postMessage
or similar.const PictureFrame = window.PictureFrameComponent || NullPictureFrame;
works fine without using hooks, which the teams have been using until now, which has reinforced the idea of very lose dependencies detailed aboveexternals
in the main-app
and loading them in that wayI see you have taken an approach to have multiple projects under one directory/repository/codebase, which I think it is not the best approach. You should split and have all your projects independently, a library sub-project where you have all the shared code/components and then make each project to have access to what it needs.
You can read an article I have wrote about Share React Native components to multiple projects, which I suggest 3 ways for sharing code between projects. I'll emphasize to the Webpack/Babel way, which you can utilize module-resolver package to configure directory aliases. You can use .babelrc
and module-resolver
to configure your outer dependencies like this:
{
"presets": ["@babel/preset-react"]
"plugins": [
"@babel/plugin-transform-runtime",
[
"module-resolver",
{
"root": ["./"],
"extensions": [".js", ".jsx", ".ts", ".tsx"],
"stripExtensions": [".js", ".jsx", ".ts", ".tsx"],
"alias": {
"@components": "./components",
"@screens": "./screens",
"@utils": "./utils",
"@data": "./data",
"@assets": "./assets",
"@app": "./",
"@myprojectlib": "../MyProject-Library",
"@myprojecti18n": "../MyProject-Library/i18n/rn",
"@myprojectbackend": "../MyProject-Backend/firebase/functions",
}
}
]
]
}
After that, you can use something like:
import { PictureFrame } from '@myprojectlib/components/PictureFrame';
and you'll have it available on each project you have configured to have access to "@myprojectlib": "../MyProject-Library"
.
With this way, you can have multiple versions of React and any package inside your project because each project will be a standalone codebase with it's own dependencies but with the same shared components/functions.
As quoted in React.js documentation,
Hooks are essentially built to access state and other react features in a classless component.
Rules to keep in mind while working with hooks
Only Call Hooks at the Top Level
Only Call Hooks from React Functions
From the details that you've posted, I believe you've already taken into consideration the above points.
For root cause analysis you probably need to set some debugging statements in your code or log messages on the console to investigate the component lifecycle.
Also, it'll be a lot better if you could reconstruct/rearrange the application as follows:
public/index.html
<body>
<div id="root" class="main-app"></div>
<script>
<!-- scrpits can be kept here -->
</script>
</body>
Create another component Main App and include sub-app-1 and sub-app-2 there.
and since you're using "react-dom": "^16.12.0"
and "react-router-dom": "^5.1.2"
,
it'll be easier for your set the routes.
Can't really say this will solve your case, but it's a good practice to include.
All the best.
I have been faced the same issue in my project, the solution is simple.
you can go to all your subproject(s) and run bellow command.
npm link ../main_app/node_modules/react
Referece: https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react
expose-loader
webpack pluginUsing the expose-loader
in the main-app
allows both sub-app-1
and sub-app-2
to use the libraries from main-app
sub-app-1
and sub-app-2
have a config-overrides.js
to indicate they rely on external
libraries
module.exports = function override(config, env) {
config.externals = config.externals || {};
config.externals.axios = "axios";
config.externals.react = "React";
config.externals["react-dom"] = "ReactDOM";
config.externals["react-router"] = "ReactRouterDOM";
return config;
}
main-app
is responsible for providing those libraries. This can be achieved either by embedding the libraries using <script />
tags or by using the expose-loader
, the config-overrides.js
effectively looks like so:
module.exports = function override(config, env) {
config.module.rules.unshift({
test: require.resolve("react"),
use: [
{
loader: "expose-loader",
options: "React"
}
]
});
config.module.rules.unshift({
test: require.resolve("react-dom"),
use: [
{
loader: "expose-loader",
options: "ReactDOM"
}
]
});
config.module.rules.unshift({
test: /\/react-router-dom\//, // require.resolve("react-router-dom"), did not work
use: [
{
loader: "expose-loader",
options: "ReactRouterDOM"
}
]
});
return config;
};
Now when I visit my main-app
page, I can see React
, ReactDOM
and ReactRouterDOM
on the window object. Now it is possible for my apps to load out of order, but now we need to ensure that the main-app
is loaded first, fortunately there is a somewhat straightfoward way. In public/index.html
I remove my referenced sub-apps
<body>
<div class="main-app"></div>
<div class="sub-app-1"></div>
<div class="sub-app-2"></div>
- <script src="sub-app-1/static/js/bundle.js" async></script>
- <script src="sub-app-1/static/js/0.chunk.js" async></script>
- <script src="sub-app-1/static/js/main.chunk.js" async></script>
- <script src="sub-app-2/static/js/bundle.js" async></script>
- <script src="sub-app-2/static/js/0.chunk.js" async></script>
- <script src="sub-app-2/static/js/main.chunk.js" async></script>
</body>
and in my index.tsx
I can dynamically load the scripts in question and wait for them
// Will dynamically add the requested scripts to the page
function load(url: string) {
return new Promise(function(resolve, reject) {
var script = document.createElement("script");
script.type = "text/javascript";
script.async = true;
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// Load Sub App 1
Promise.all([
load(`${process.env.SUB_APP_1_URL}/static/js/bundle.js`),
load(`${process.env.SUB_APP_1_URL}/static/js/0.chunk.js`),
load(`${process.env.SUB_APP_1_URL}/static/js/main.chunk.js`)
]).then(() => {
console.log("Sub App 1 has loaded");
window.dispatchEvent(new Event("SubAppOne.Initialized"));
const SubAppOne = window.SubAppOne;
ReactDOM.render(<SubAppOne />, document.querySelector(".sub-app-1"))
});
// Load Sub App 2
Promise.all([
load(`${process.env.SUB_APP_2_URL}/static/js/bundle.js`),
load(`${process.env.SUB_APP_2_URL}/static/js/0.chunk.js`),
load(`${process.env.SUB_APP_2_URL}/static/js/main.chunk.js`)
]).then(() => {
console.log("Sub App 2 has loaded");
window.dispatchEvent(new Event("PictureFrameComponent.Initialized")); // Kicks off the rerender of the Stub Component
window.dispatchEvent(new Event("SubAppTwo.Initialized"));
const SubAppTwo = window.SubAppTwo;
ReactDOM.render(<SubAppTwo />, document.querySelector(".sub-app-2"))
});
Now main-app
has all the shared dependencies like React
, and will load the two sub-apps
when the dependencies are ready. The PictureFrameComponent
now works as expected with hooks! It would be preferrable if the loading could happen in any order, i.e. sub-app-1
could be loaded before main-app
, however given the importance of the main-app
and the minor addition in functionality provided by the PictureFrameComponent
this could be a passable solution for some projects.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With