DEV Community

Lev Eidelman Nagar
Lev Eidelman Nagar

Posted on

3

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.

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (2)

Collapse
 
scriptedalchemy profile image
Zack Jackson • Edited

We recently released v2 which includes a more powerful runtime api.
Pretty much anything you want to do can be done via the runtime or runtime plugin apis we added.

module-federation.io/guide/basic/r...
module-federation.io/plugin/dev/in...
module-federation.io/guide/basic/w...

Collapse
 
eidellev profile image
Lev Eidelman Nagar

Thanks for all your work. We are planning to upgrade to v2 and rspack as soon as possible.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay