DEV Community

Lev Eidelman Nagar
Lev Eidelman Nagar

Posted on

Joining the (Module) Federation

Image description

Introduction

Module federation is a way of loading remote modules in runtime (as opposed to during the build process) and is a common way of implementing micro-frontends in modern web applications which use webpack.

In this guide I will show you how you can load any remote module at runtime.

I assume that you have at least passing familiarity with the basics of webpack and module federation.

If this is a new topic for you I highly recommend reading the official webpack documentation and visiting module-federation.io/ for more information.

When You Can't Commit

Most module federation examples and tutorials out there will have you configure module federation for your host application like so:

// webpack.config.js

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        app1: 'app1@http://localhost:3001/remoteEntry.js',
        // ...additional remotes
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

While this will work locally, it does not take into account the following:

  1. Different environments(e.g. staging, production)
  2. Needing to load an arbitrary remote from an unknown(at least during build time) URL

Although the first can be solved by using external-remotes-plugin, handling arbitrary remotes is less obvious.

I should also note that configuring the remotes this way means that all remotes will be loaded when the page initially starts. This means the user will incur the cost of having to load additional Javascript files even if they never use any of it.

A Little Helper Goes a Long Way

I created a small utility function that accepts the URL of the remote module or a promise that resolves to it, loads the remote script by creating a new DOM <script> element, initializes the module and returns it.

type RemoteAppSettings = {
  scope: string;
  module: string;
};

/**
 * Loads a remote application module
 * See [Dynamic Remote Containers](https://webpack.js.org/concepts/module-federation/#dynamic-remote-containers)
 * for more information on how this works
 * @param url Remote app URL or function that resolved to a URL
 * @param app Remote app settings
 * @returns Remote module
 */
export function loadRemoteApp(
  url: string | (() => Promise<string>),
  app: RemoteAppSettings
) {
  const { scope, module } = app;

  return (): Promise<{ default: ComponentType<any> }> =>
    new Promise(async (resolve, reject) => {
      const element = document.createElement('script');

      let resolvedUrl: string;

      if (typeof url === 'string') {
        resolvedUrl = url;
      } else {
        resolvedUrl = await url();
      }

      element.src = resolvedUrl;
      element.type = 'text/javascript';
      element.async = true;

      element.onload = async () => {
        // Initializes the share scope.
        const container = window[scope];
        // Initialize the container, it may provide shared modules
        await container.init(__webpack_share_scopes__.default);
        const factory = await window[scope].get(module);
        const Module = factory();

        resolve(Module);
      };

      element.onerror = err => {
        reject(
          new Error(
            `Failed to initialize remote Application\nURL: ${url}\nScope: ${scope}\nModule: ${module}`,
            { cause: err }
          )
        );
      };

      document.head.appendChild(element);
    });
}
Enter fullscreen mode Exit fullscreen mode

Now we can remove all pre-configured remotes from our webpack.config.js:

// webpack.config.js

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {},
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

And in our React application we can use this helper to load a remote component and render it with lazy and <Suspense> like this:

import { getRemoteUrl, loadRemoteApp } from 'common/utils/moduleFederation';
import ErrorBoundary from 'components/ErrorBoundary';
import { Loader } from 'connected-components/Loader/Loader';
import { ComponentType, Suspense, lazy } from 'react';
import { RemoteComponentProps } from 'types/micro-frontends';

const RemotApp = lazy<ComponentType<RemoteComponentProps>>(
  loadRemoteApp(
    // Call the backend to get the remote URL
    getRemoteUrl('component-name'),
    {
      scope: 'remote',
      module: './App'
    }
  )
);

export function RemoteComponent() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<Loader />}>
        <RemoteApp/>
      </Suspense>
    </ErrorBoundary>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now the remote module will not be loaded until required and we can use any valid remote URL.

Top comments (0)