8

What is happening is that I am consuming a Micro front-end through module federation, but even using React.Lazy the bundle (remoteEntry) is loaded at the start of the application, without accessing the route where the component is being imported.

To simulate the scenario you can access the repository and follow the steps described. click here to access.

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');
const dependencies = require('./package.json').dependencies;
const ModuleFederationPlugin =
  require('webpack').container.ModuleFederationPlugin;
const path = require('path');
const dotenv = require('dotenv');

module.exports = (_, args) => {
  return {
    mode: args.mode,
    entry: './src/index.js',
    output: {
      filename:
        process.env.NODE_ENV === 'development'
          ? 'main.js'
          : 'main.[contenthash].js',
      publicPath: process.env.NODE_ENV === 'development' ? '/' : '/app/',
      path: path.resolve(__dirname, 'build')
    },
    devServer: {
      static: {
        directory: path.join(__dirname, 'build')
      },
      port: 3001,
      historyApiFallback: true
    },
    resolve: {
      extensions: ['.ts', '.tsx', '.js', '.jsx'],
      alias: {
        actions: path.resolve(__dirname, 'src', 'actions'),
        components: path.resolve(__dirname, 'src', 'components'),
        containers: path.resolve(__dirname, 'src', 'containers'),
        'custom-hooks': path.resolve(__dirname, 'src', 'custom-hooks'),
        enums: path.resolve(__dirname, 'src', 'enums'),
        helpers: path.resolve(__dirname, 'src', 'helpers'),
        hooks: path.resolve(__dirname, 'src', 'hooks'),
        images: path.resolve(__dirname, 'src', 'images'),
        libs: path.resolve(__dirname, 'src', 'libs'),
        middlewares: path.resolve(__dirname, 'src', 'middlewares'),
        reducers: path.resolve(__dirname, 'src', 'reducers'),
        sagas: path.resolve(__dirname, 'src', 'sagas'),
        services: path.resolve(__dirname, 'src', 'services'),
        store: path.resolve(__dirname, 'src', 'store'),
        views: path.join(__dirname, 'src', 'views'),
        routes: path.join(__dirname, 'src', 'routes')
      },
      fallback: {
        fs: false,
        tls: false,
        net: false,
        path: false,
        zlib: false,
        http: false,
        https: false,
        stream: false,
        crypto: false,
        'styled-components': require.resolve('styled-components'),
        util: false
      }
    },
    module: {
      rules: [
        {
          test: /\.jsx?$/,
          loader: 'babel-loader',
          exclude: /node_modules/,
          options: {
            presets: ['@babel/preset-react']
          }
        },
        {
          test: /\.css$/i,
          use: ['style-loader', 'css-loader']
        },
        {
          test: /\.s[ac]ss$/i,
          use: ['style-loader', 'css-loader', 'sass-loader']
        },
        {
          test: /\.(png|jpg|jpeg|gif|pdf)$/,
          exclude: /node_modules/,
          use: ['file-loader?name=[name].[ext]']
        },
        {
          test: /\.svg$/,
          use: ['@svgr/webpack', 'url-loader']
        }
      ]
    },

    plugins: [
      new webpack.ProgressPlugin(),
      new CleanWebpackPlugin(),
      new CopyPlugin({
        patterns: [{ from: 'public/config.json', to: 'config.json' }]
      }),
      new ModuleFederationPlugin({
        name: 'connect_front',
        remotes: {
          connect_vim_front: `promise new Promise((resolve, reject) => fetch('${process.env.PUBLIC_URL}config.json', {
            method: 'GET'
          })
            .then(resp => {
                            return resp.text();
                        }).then((resp) => {
                            const parsedResp = JSON.parse(resp);
                            const script = document.createElement('script');
                            script.src = parsedResp.REACT_APP_URL_CONNECT_VIM_FRONT || "/",
                            script.onload = () => {
                                const proxy = {
                                    get: (request) => window.connect_vim_front.get(request),
                                    init: (arg) => {
                                        try {
                                            return window.connect_vim_front.init(arg)
                                        } catch(e) {
                                            console.error('remote container already initialized')
                                        }
                                    }
                                }
                                resolve(proxy)
                            }

                            script.onerror = function() {
                                reject();
                            };

                            document.head.appendChild(script)
                        })
            .catch(err => console.log(err)))`
        },
        shared: {
          ...dependencies,
          react: {
            singleton: true,
            eager: true,
            requiredVersion: dependencies.react
          },
          'react-dom': {
            singleton: true,
            eager: true,
            requiredVersion: dependencies['react-dom']
          }
        }
      }),
      new HtmlWebpackPlugin({
        template: './public/index.html',
        filename: './index.html',
        favicon: './public/favicon.ico',
        title: 'Caching'
      }),
      new webpack.ProvidePlugin({
        process: 'process/browser'
      }),
      new webpack.DefinePlugin({
        'process.env': JSON.stringify(
          dotenv.config({
            path: `${
              process.env.ENVIRONMENT
                ? `.env.${process.env.ENVIRONMENT}`
                : '.env'
            }`
          }).parsed
        )
      })
    ]
  };
};

Route

import React, { Suspense } from 'react';

import Route from './Route';

const Vim = React.lazy(() => import('views/vim'));

export const AuthenticatedRoutes = ({ skipRedirect }) => {
  return (
    <>
      <Route
        exact
        path='/...'
        component={component}
      />
      <Route
        skipRedirect={skipRedirect}
        isPrivate
        exact
        path='/vim'
        component={
          <Suspense fallback={<div>Loading...</div>}>
            <Vim />
          </Suspense>
        }
      />
      <Route
        exact
        path='/...'
        component={component}
      />
    </>
  );
};

Page Component

import React from 'react';
import ModuleLoader from '../../components/ModuleLoader';
import { FallbackLoading } from '../../components/FallbackLoading';

import { VimPageWrapper } from './VimPageWrapper';

const Vim = () => {
  return (
    <ModuleLoader fallback={<FallbackLoading />}>
      <VimPageWrapper />
    </ModuleLoader>
  );
};

export default Vim;

Consuming the MFE

import React from 'react';
import ModuleLoader from 'components/ModuleLoader';
import { FallbackLoading } from 'components/FallbackLoading';
const VimPage = React.lazy(() => import('connect_vim_front/vim-page'));
const Wrapper = React.lazy(() => import('connect_vim_front/Wrapper'));

export const VimPageWrapper = () => {
  return (
    <ModuleLoader fallback={<FallbackLoading />}>
      <Wrapper>
        <VimPage />
      </Wrapper>
    </ModuleLoader>
  );
};

Bundle(remoteEntry) loaded even not accessing the route insert the description of the image here enter image description here

ModuleLoader

import React, { Suspense } from 'react';
import { FallbackError } from '../FallbackError';

class ModuleLoader extends React.Component {
  constructor(props) {
    super(props);

    this.state = { hasError: false };
  }

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  render() {
    const { fallback, children } = this.props;
    const { hasError } = this.state;

    if (hasError) {
      return <FallbackError />;
    }

    return <Suspense fallback={fallback}>{children}</Suspense>;
  }
}

export default ModuleLoader;
JBallin
  • 8,481
  • 4
  • 46
  • 51
andre.godasi
  • 91
  • 2
  • 7
  • 2
    @Ethan I edited the question, to add the codes and remove the images, I created a repository to simulate the error, thank you very much for your comment – andre.godasi Apr 30 '22 at 04:49

1 Answers1

0

You can use this package module-federation-import-remote

import { importRemote } from "module-federation-import-remote";

  // If it's a regular js module:
  importRemote({ url: "http://localhost:3001", scope: 'Foo', module: 'Bar' }).then(({/* list of Bar exports */}) => {
    // Use Bar exports
  });

  // If Bar is a React component you can use it with lazy and Suspense just like a dynamic import:
  const Bar = lazy(() => importRemote({ url: "http://localhost:3001", scope: 'Foo', module: 'Bar' }));

  return (
    <Suspense fallback={<div>Loading Bar...</div>}>
      <Bar />
    </Suspense>
  );

to import a Vue component into a React app for example:

instead of configuring it in webpack.config.js

remotes: {
     DashboardApp: "DashboardApp@http://localhost:9005/remoteEntry.js",
},

create a component like this

export default (props) => {
    const ref = useRef(null);
    
    useEffect(() => {
        importRemote({ url: "http://localhost:9005", scope: 'DashboardApp', module: 'DashboardPage' })
            .then(({mount}) => {
                mount(ref.current, props)
        });
    }, [])

    return <div ref={ref} />
}
Leonardo Lima
  • 474
  • 5
  • 12