Reputation: 131
I'm trying to get a dynamic system in runtime with the help of Module Federation (webpack 5 feature). Everything works great, but when I add hooks to the 'producer' module (the module from which the host application dynamically imports the component) I get a mass of 'invalid rule of hooks' errors.
Warning: Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. You can only call Hooks at the top level of your React function. For more information, see [LINK RULES OF HOOKS]
Warning: React has detected a change in the order of Hooks called by PluginHolder. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: [LINK RULES OF HOOKS]
I've already used externals field and added script tag in html files, I've used shared option with adding singleton field: true and specifying react and react-dom version Each time the console spits out a mass of errors
This is my method straight from the documentation to load the module
const loadComponent = (scope: string, module: string) => async (): Promise<any> => {
// @ts-ignore
await __webpack_init_sharing__('default');
// @ts-ignore
const container = window[scope];
// @ts-ignore
await container.init(__webpack_share_scopes__.default);
// @ts-ignore
const factory = await window[scope].get(module);
return factory();
};
To load the remoteEntry.js file I use makeAsyncScriptLoader HOC with react-async-script like this:
const withScript = (name: string, url: string) => {
const LoadingElement = () => {
return <div>Loading...</div>;
};
return () => {
const [scriptLoaded, setScriptLoaded] = useState<boolean>(false);
const AsyncScriptLoader = makeAsyncScriptLoader(url, {
globalName: name,
})(LoadingElement);
if (scriptLoaded) {
return <PluginHolder name={name}/>;
}
return (
<AsyncScriptLoader
asyncScriptOnLoad={() => {
setScriptLoaded(true);
}}
/>
);
};
};
PluginHolder is simple component which wraps loading module from loaded script (loading is done in effect)
useEffect((): void => {
(async () => {
const c = await loadComponent(name, './Plugin')();
setComponent(c.default);
})();
}, []);
return cloneElement(component);
And on top of that is starter:
const [plugins, setPlugins] = useState<PluginFunc[]>([]);
useEffect((): void => {
pluginNames.forEach(desc => {
const loaded = withScript(desc.name, desc.url);
setPlugins([...plugins, loaded]);
});
}, []);
I do not use React.Lazy because I cannot use import(). What's more, in host application I set field eager: true in react and react-dom
My webpack.config.js (host) below:
require('tslib');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { DefinePlugin } = require('webpack');
const { ModuleFederationPlugin } = require('webpack').container;
// @ts-ignore
const AutomaticVendorFederation = require('@module-federation/automatic-vendor-federation');
const packageJson = require('./package.json');
const exclude = ['babel', 'plugin', 'preset', 'webpack', 'loader', 'serve'];
const ignoreVersion = ['react', 'react-dom'];
const automaticVendorFederation = AutomaticVendorFederation({
exclude,
ignoreVersion,
packageJson,
shareFrom: ['dependencies', 'peerDependencies'],
ignorePatchVersion: false,
});
module.exports = {
mode: 'none',
entry: {
app: path.join(__dirname, 'src', 'index.tsx'),
},
target: 'web',
resolve: {
extensions: ['.ts', '.tsx', '.js'],
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: '/node_modules/',
use: 'ts-loader',
},
{
test: /\.(s[ac]|c)ss$/i,
exclude: '/node_modules/',
use: [
'style-loader',
'css-loader',
'sass-loader',
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'public', 'index.html'),
favicon: path.join(__dirname, 'public', 'favicon.ico'),
}),
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development'),
}),
new ModuleFederationPlugin({
name: 'host',
remotes: {},
exposes: {},
shared: {
...automaticVendorFederation,
react: {
eager: true,
singleton: true,
requiredVersion: packageJson.dependencies.react,
},
'react-dom': {
eager: true,
singleton: true,
requiredVersion: packageJson.dependencies['react-dom'],
},
},
}),
],
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
publicPath: 'http://localhost:3001/',
},
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: 3001,
},
};
And also my webpack.config.js from second module:
require('tslib');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { DefinePlugin } = require('webpack');
const { ModuleFederationPlugin } = require('webpack').container;
// @ts-ignore
const AutomaticVendorFederation = require('@module-federation/automatic-vendor-federation');
const packageJson = require('./package.json');
const exclude = ['babel', 'plugin', 'preset', 'webpack', 'loader', 'serve'];
const ignoreVersion = ['react', 'react-dom'];
const automaticVendorFederation = AutomaticVendorFederation({
exclude,
ignoreVersion,
packageJson,
shareFrom: ['dependencies', 'peerDependencies'],
ignorePatchVersion: false,
});
module.exports = (env, argv) => {
const { mode } = argv;
const isDev = mode !== 'production';
return {
mode,
entry: {
plugin: path.join(__dirname, 'src', 'index.tsx'),
},
target: 'web',
resolve: {
extensions: ['.ts', '.tsx', '.js'],
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: '/node_modules/',
use: 'ts-loader',
},
{
test: /\.(s[ac]|c)ss$/i,
exclude: '/node_modules/',
use: [
'style-loader',
'css-loader',
'sass-loader',
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'public', 'index.html'),
favicon: path.join(__dirname, 'public', 'favicon.ico'),
}),
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development'),
}),
new ModuleFederationPlugin({
name: 'example',
library: { type: 'var', name: 'example' },
filename: 'remoteEntry.js',
remotes: {},
exposes: {
'./Plugin': './src/Plugin',
},
shared: {
...automaticVendorFederation,
react: {
eager: isDev,
singleton: true,
requiredVersion: packageJson.dependencies.react,
},
'react-dom': {
eager: isDev,
singleton: true,
requiredVersion: packageJson.dependencies['react-dom'],
},
},
}),
],
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: 'http://localhost:3002/',
},
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: 3002,
},
};
};
Do you have any experience with it or any clues - I think the point is that 2 applications use 2 instances of the react but these are my guess. Is there something wrong with my configuration?
Upvotes: 13
Views: 17196
Reputation: 186
Make sure you are adding in shared dependencies to your webpack.config file.
See below for example:
plugins: [
new ModuleFederationPlugin(
{
name: 'MFE1',
filename:
'remoteEntry.js',
exposes: {
'./Button':'./src/Button',
},
shared: { react: { singleton: true }, "react-dom": { singleton: true } },
}
),
new HtmlWebpackPlugin({
template:
'./public/index.html',
}),
],
};
I have both the host and remote project setup with this shared property. Fixed it for me when hooks broke my host app. It's because there are duplicate react dependencies, regardless if the version are the same you will get this error.
Upvotes: 3