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?
Webpack 5 module federation is a way provided by Webpack team to load multiple micro applications(remote applications) into another application(host application) which makes an advantage of managing all the applications separately from development to deployment.
__webpack_init_sharing__ : This is a Webpack default variable that initializes the shared scope and adds all the known provided modules from the local build or the remote container build. __webpack_share_scopes__ : This is also a default Webpack variable, which initializes the exposed module or the container.
The ModuleFederationPlugin allows a build to provide or consume modules with other independent builds at runtime. const { ModuleFederationPlugin } = require('webpack').
A Vite plugin which support Module Federation. Inspired by Webpack Module Federation feature.
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.
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