Dhana D.
Dhana D.

Reputation: 1720

Webpack Module Federation handling when the remote app is down/unavailable

I am currently exploring about micro frontend with Module Federation. I just forked a sandbox, tried it with success when the both modules available. It has 2 modules, app1 as the host, and app2 as the remote component. But as I think that each modules in module federation should be independent, I tried to make the app2 unavailable as I didn't start it. Therefore I got error when I run the app1, it finished loading with displaying the fallback of the React's Suspense, but milliseconds later, it becomes blank as there's error I can't retrieve thus I don't really know.

After that, I tried Webpack's Promise Based Dynamic Remotes, then my webpack-config.js becomes like this:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const ExternalTemplateRemotesPlugin = require('external-remotes-plugin');
const path = require('path');

module.exports = {
  entry: './src/index',
  mode: 'development',
  devServer: {
    static: path.join(__dirname, 'dist'),
    port: 3001,
  },
  output: {
    publicPath: 'auto',
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['@babel/preset-react'],
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      remotes: {
        app2: Promise((resolve) => {
          const urlParams = new URLSearchParams(window.location.search);
          const version = urlParams.get('app1VersionParam');
          // This part depends on how you plan on hosting and versioning your federated modules
          const remoteUrlWithVersion = '[app2Url]' + '/remoteEntry.js';
          const script = document.createElement('script');
          script.src = remoteUrlWithVersion;
          script.onload = () => {
            // the injected script has loaded and is available on window
            // we can now resolve this Promise
            const proxy = {
              get: (request) => window.app1.get(request),
              init: (arg) => {
                try {
                  return window.app1.init(arg);
                } catch (e) {
                  console.log('remote container already initialized');
                }
              },
            };
            resolve(proxy);
          };
          // inject this script with the src set to the versioned remoteEntry.js
          document.head.appendChild(script);
        }),
        // "app2@[app2Url]/remoteEntry.js",
      },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
    }),
    new ExternalTemplateRemotesPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

I tried start the app1 again, then this error comes:

$ webpack serve
[webpack-cli] Failed to load '/home/projects/github-rl5uyr/app1/webpack.config.js' config
[webpack-cli] TypeError: undefined is not a promise
    at Promise (<anonymous>)
    at Object.eval (/home/projects/github-rl5uyr/app1/webpack.config.js:32:15)
    at Object.function (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:11:114831)
    at Module._compile (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:6:167880)
    at Object.Module._extensions..js (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:6:168239)
    at Module.load (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:6:166317)
    at Function.Module._load (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:6:163857)
    at Module.require (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:6:166635)
    at i (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:6:427483)
    at _0x5301a6 (https://github-rl5uyr.w.staticblitz.com/blitz.01faa899fac41642342f4b7113feacabea334fa1.js:11:114450)

So, can the module federations run independently each other? If not, what's the real difference as normal library dependencies of monolith front end instead of this sophisticated micro frontend, that I assumed it should be able to work independently like microservices?

Upvotes: 6

Views: 15212

Answers (3)

D&#252;rrani
D&#252;rrani

Reputation: 1246

Problem: TypeError: Failed to fetch dynamically imported module:
I had this when trying to load an unavailable module while loading it lazily.

const Counter = lazy(() => import("remoteApp/Counter")); // it may break!

I solved it natively on the react side WITHOUT adding any extra dependencies by catching the error on the import statement like so:

import { lazy, Suspense } from "react";

const Counter = lazy(() =>
  import("remoteApp/Counter").catch(() => ({
    default: ({ onLoadingError }: { onLoadingError: (error: Error) => void }) => {
      onLoadingError(new Error("Unable to load Counter"));
      return <h5>Unable to load Counter, please reload the page!</h5>;
    },
  }))
);

export default function App() {
  function errorHandler(e: Error) {
    console.error(e.message);
    // Your app is saved from bowing up!
    // Do whatever you want here...
  }

  return (
    <>
      <h1>Host App</h1>
      <div className="card">
        <Suspense fallback="Loading Counter...">
          <Counter onLoadingError={errorHandler} />
        </Suspense>
      </div>
    </>
  );
}

Counter is a regular react component.
If the error occurs it will return a fallback UI in this case:

<h5>Unable to load Counter, please reload the page!</h5>

I passed a custom error handler named onLoadingError to inform my business logic that the remote component failed to load.

Upvotes: 0

Edwin Aquino
Edwin Aquino

Reputation: 219

I faced the same problem and the solution I found was not in the webpack.config.js but in the import of the modules in react. The first thing is to install these 2 dependencies

npm i react-lazily (https://www.npmjs.com/package/react-lazily)
npm i react-error-boundary (https://www.npmjs.com/package/react-error-boundary)

The components or views that you want to import from any of your apps would be called with lazily, for example

const { Login }      = lazily(() => import("auth/Auth"));
const { Directory }  = lazily(() => import("directory/Directory"));
const { Resources }  = lazily(() => import("resources/Resources"));
const { Statistics } = lazily(() => import("statistics/Statistics"));

For the use of react-error-boundary I would recommend making a separate component, so I did

import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

export const Externals = ({ children }) => 
{
    return (
        <Suspense 
            fallback="loading...."
        >
            <ErrorBoundary>
                { children }
            </ErrorBoundary>
        </Suspense>
    )
}

In my case I made use of this component in my AppRouter.jsx file as follows

import React from 'react';
import { createBrowserRouter, Navigate } from "react-router-dom";
import { lazily } from 'react-lazily'

import { Authentification, AuthentificationChecked } from '../views/grid/index';
import { Externals } from './Externals';

const { Login }          = lazily(() => import("auth/Auth"));
const { Directory }  = lazily(() => import("directory/Directory"));
const { Resources }  = lazily(() => import("resources/Resources"));
const { Statistics } = lazily(() => import("statistics/Statistics"));

export const router = createBrowserRouter(
[
    {
        path: '/*', element: <Navigate to="auth/login" />
    },
    {
        path: 'auth', 
        element: <Authentification />,
        children:
        [
            { path: 'login', element: <Externals><Login /></Externals>, errorElement: <div>Error al cargar el modulo</div> }
        ]
    },
    {
        path: 'main', 
        element: <AuthentificationChecked />,
        children:
        [
            { path: 'directory', element: <Externals><Directory /></Externals>, errorElement: <div>Error al cargar el modulo</div> },
            { path: 'resources', element: <Externals><Resources /></Externals>, errorElement: <div>Error al cargar el modulo</div> },
            { path: 'statistics', element: <Externals><Statistics /></Externals>, errorElement: <div>Error al cargar el modulo</div> }
        ]
    }
]);

Where <Externals /> encapsulates my component that I import from any of the apps, if it responds correctly in the view the component will be reflected, but if an error occurs the application that imports it will not hang or stay in white, in its case it will show the error message that is included in errorElement of each route.

And this without having to do any extra configuration in the webpack.config.js, this is how my file looks

  remotes: 
  {
    auth: "auth@http://localhost:8081/remoteEntry.js", 
    directory: "directory@http://localhost:8082/remoteEntry.js",
    resources: "resources@http://localhost:8083/remoteEntry.js",
    statistics: "statistics@http://localhost:8084/remoteEntry.js"
  },

Upvotes: 1

Mattia SaxaGogo Rossi
Mattia SaxaGogo Rossi

Reputation: 41

I think there are some stuff to be fixed in that promise definition: first of all it should be defined as a string

app2: new Promise((resolve) => { .... Where the configuration passed for app2 is a string between ` character

As stated in the docs

After that you should change your proxy definition to fetch window.app2 not window.app1.

Finally remoteUrlWithVersion = '[app2Url]' + '/remoteEntry.js'; is not a valid URL

Hope it helps

Upvotes: 1

Related Questions