Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Webpack 5 module federation - hooks in remote module - not working

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?

like image 472
Piturzasty Avatar asked Sep 17 '20 16:09

Piturzasty


People also ask

What is module Federation Webpack 5?

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.

What is __ Webpack_init_sharing __?

__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.

What is ModuleFederationPlugin?

The ModuleFederationPlugin allows a build to provide or consume modules with other independent builds at runtime. const { ModuleFederationPlugin } = require('webpack').

Does Vite support module federation?

A Vite plugin which support Module Federation. Inspired by Webpack Module Federation feature.


1 Answers

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.

like image 83
anonymous Avatar answered Nov 15 '22 03:11

anonymous