Joining the (Module) Federation
Tags: javascript, react, webpack, modulefederation
Published: 2024-02-28 18:00:00


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/knki6flz6d03h2t3u5cu.png)


# Introduction
[Module federation](https://webpack.js.org/concepts/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 ](https://webpack.js.org/concepts/module-federation/) and visiting [module-federation.io/](https://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:

```javascript
// webpack.config.js

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

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](https://www.npmjs.com/package/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.

```typescript
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);
    });
}
```

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

```javascript
// webpack.config.js

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

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

```typescript
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>
  );
}
```
Now the remote module will not be loaded until required and we can use any valid remote `URL`.