Search code examples
reactjsmicro-frontendwebpack-5

Components with same import in micro-frondends


I am building a React application based on micro-frondends using ModuleFederationPlugin ModuleFederationPlugin from webpack 5.

I have two separated projects (App1 and App2) which expose components and these are used in other project (AppShell).

App1 structure:

components/Button.tsx
App1.tsx

App2 structure:

components/Button.tsx
App2.tsx

Both projects contain component Button.tsx which means that in both project this component has same export. This component is used in App1.tsx (App2.tsx).

Now if I want to use App1 and App2 in AppShell, I always see the Button from App1:

enter image description here

App1 webpack config (App2 is similar):

const HtmlWebpackPlugin = require("html-webpack-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");


module.exports = {
    entry: "./src/index.tsx",
    output: {
        filename: "test-app1.[contenthash].js",
    },

    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.json'],
    },
    module: {
        rules: [
            {
                test: /\.(ts|js)x?$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
            },
        ],
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({template: './public/index.html'}),
        new ModuleFederationPlugin({
            name: "remoteApp",
            library: { type: "var", name: "remoteApp" },
            filename: "remoteEntry.js",
            exposes: {
                "./MyApp": "./src/MyApp",
            },
            shared: ["react", "react-dom", "@material-ui/core"],
        })
    ]};

AppShell webpack config:

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");


module.exports = {
    entry: "./src/index.tsx",
    output: {
        filename: "app_shell.[contenthash].js",
        path: path.resolve(__dirname, "dist"),
        publicPath: ""
    },

    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.json'],
    },
    module: {
        rules: [
            {
                test: /\.(ts|js)x?$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
            },
            {
                test: /bootstrap\.js$/,
                loader: "bundle-loader",
                options: {
                    lazy: true,
                },
            },
        ],
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({template: './public/index.html'}),
        new ModuleFederationPlugin({
            name: "app_shell",
            remotes: {
                "remoteApp-mfe": "remoteApp@http://localhost:8001/remoteEntry.js",
                "remoteApp2-mfe": "remoteApp2@http://localhost:8002/remoteEntry.js",
            },
            shared: {react: {singleton: true}, "react-dom": {singleton: true}, "@material-ui/core": {singleton: true}},
        }),

    ]};

Components usage in AppShell:

import React from "react";

export const AppShell: React.FC = () => {
  // @ts-ignore
  const App1 = React.lazy(() => import("remoteApp-mfe/MyApp"));
  // @ts-ignore
  const App2 = React.lazy(() => import("remoteApp2-mfe/MyApp2"));

  return (
    <>
      <h1>App Shell</h1>
      <React.Suspense fallback="Loading...">
        <App1 />
      </React.Suspense>
      <React.Suspense fallback="Loading...">
        <App2 />
      </React.Suspense>
    </>
  );
};

You can download the test application here.

Do you have any idea how to fix this problem, please? (I know that I can refactor the apps but it is not the solution I am looking for ;)). Thank you.


Solution

  • The error lies in the package names of your individual applications. Each package.json in your app folders has the name "test":

    app-shell/
      public/
      src/
      package.json -> package name = "test"
      ...
    test-app-01/
      public/
      src/
      package.json -> package name = "test"
      ...
    test-app-02/
      public/
      src/
      package.json -> package name = "test"
      ...
    

    When your applications getting stitched together on the client-side, all application code from all 3 packages get merged under the same scope "test". The problem occurs now because the apps have partly the same folder and file structure.

    test-app-01/
      public/
      src/
        components/
          MyButton.tsx
        // other code
      ...
    test-app-02/
      public/
      src/
        components/
          MyButton.tsx
        // other code
      ...
    

    Due to the merging on the client-side under the same scope "test" the button code gets overridden by either one of the two test-apps like so:

    test/
      src/
        components/
          MyButton.tsx // MyButton.tsx from test-app-01 or test-app-02 gets overridden
        // other code of app-shell, test-app-01 and test-app-02.
    

    The solution would be to give all the applications individual package names in the package.json's.