How to Use Module Federation with Re.Pack 3
Tags: opensource, reactnative, repack, modulefederation
Published: 2022-10-12 08:54:45

by [Jakub Binda](https://www.callstack.com/team/jakub-binda) & Andrew Andilevko

## Introduction
Along with the release of [Re.Pack](https://www.callstack.com/open-source/re-pack) v.3 there comes a huge update for the library - stable support for Module Federation.

This feature can be a powerful ally for development departments, especially when it comes to building complex apps with Micro-Frontend architecture, requiring multiple teams to deliver the whole thing. 

In this article, you’ll find out

- what’s the idea behind Module Federation,
- and how to use Module Federation with Re.Pack 3.

## The idea behind Module Federation
Module Federation was first introduced in [Webpack 5](https://webpack.js.org/concepts/module-federation/). It’s a functionality that allows for code-splitting and sharing the split code parts (chunks) between loosely coupled applications. It also helps distributed teams to ship large applications faster. And, along with its latest update, Re.Pack 3 supports this functionality out-of-the-box.

Module Federation is one of the approaches to creating **Micro-frontends** architecture for your application which makes the functionality a great ally in, for example, [super app development](https://www.callstack.com/super-app-development) or creating features on demand. 

## Micro-frontends
Going briefly through the Micro-Frontends working scheme, there are:

- **the host app** that runs firstly on the device, 
- **and Micro-frontend (MFE)** apps that are used by the host app.

That means each container (MFE) that is used by the host application could be deployed and maintained independently. The host apps are able to reflect changes in Micro-frontend immediately with no need for re-deploy until the changes are connected to the JS only.

Accessing remote JS code exposed by MFE is the key feature of Module Federation and the process is called **Runtime Deployment**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lznbsu9y9px8v6cfpnic.png)
_Scheme 1. Module Federation in the app_

**How to use Module Federation in Re.Pack**
If you are familiar with the Webpack config for Module Federation on the web environment, you will notice a similarity in the configuration using Re.Pack. The major difference is to take into account mobile platform / React Native specifics.

Let’s assume that we have the host application, two container applications, and one module used in the first container application. Each of the apps could be deployed independently, but all of them are bound using Webpack config.

The most interesting properties for us from this config are **plugins**. It is an array of all the required Webpack plugins. As you can see from the code snippet below, Re.Pack exports a `ModuleFederationPlugin` plugin that enables Module Federation functionality:

```
new Repack.plugins.ModuleFederationPlugin({
        name: 'host',
        shared: {
          react: {
            ...Repack.Federated.SHARED_REACT,
            requiredVersion: '17.0.2',
          },
          'react-native': {
            ...Repack.Federated.SHARED_REACT_NATIVE,
            requiredVersion: '0.68.2',
          },
        },
      })
```
[Host app Webpack config Module Federation plugin.js](https://gist.github.com/callstack-bot/136e6a205619915099d1f09196e5f90c#file-host-app-webpack-config-module-federation-plugin-js) hosted with ❤ by GitHub

_Code snippet 1. Host app Webpack config Module Federation plugin_

```
new Repack.plugins.ModuleFederationPlugin({
        name: 'app1',
        exposes: {
          './App': './src/App.tsx',
          './Text': './src/Text.tsx',
          './foo': './src/foo.ts',
        },
        shared: {
          react: {
            ...Repack.Federated.SHARED_REACT,
            eager: STANDALONE,
          },
          'react-native': {
            ...Repack.Federated.SHARED_REACT_NATIVE,
            eager: STANDALONE,
            requiredVersion: '0.68.2',
          },
        },
        remotes: {
          module1: 'module1@dynamic',
        },
      })
```
[Remote Container 1 Webpack config Module Federation plugin.js](https://gist.github.com/callstack-bot/ed59a8eb8a30bda1ef670c446eaa6a00#file-remote-container-1-webpack-config-module-federation-plugin-js) hosted with ❤ by GitHub

_Code snippet 2. Remote Container 1 Webpack config Module Federation plugin_

```
new Repack.plugins.ModuleFederationPlugin({
        name: 'module1',
        exposes: {
          './Root': './src/Root.tsx',
          './baz': './src/baz.ts',
        },
        shared: {
          react: {
            ...Repack.Federated.SHARED_REACT,
            eager: STANDALONE,
          },
          'react-native': {
            ...Repack.Federated.SHARED_REACT_NATIVE,
            eager: STANDALONE,
            requiredVersion: '0.68.2',
          },
        },
        remotes: {
          app1: 'app1@dynamic',
        },
      })
```
[Module app Webpack config Module Federation plugin.js](https://gist.github.com/callstack-bot/4be57c16c85df2173015c693bb1566a0#file-module-app-webpack-config-module-federation-plugin-js) hosted with ❤ by GitHub

_Code snippet 3. Module app Webpack config Module Federation plugin_

Going through the properties in the Module Federation plugin config, we can find the same ones in all of the apps. The `name` property is required, so we have to provide a unique name for each of our apps. Then, we will use it across the app for importing federated modules. The next common property is called `shared`. This array contains libraries and provides Webpack information regarding the libraries that should be shared in other applications. Also, they should have only one instance across the apps. Re.Pack provides default settings for React and React Native libraries in `shared` property.

So, until there are no more other libraries that should be shared, this property could be skipped. Dependency defined inside shared property might be also set as `eager`, which doesn’t put the modules in an async chunk, but provides them synchronously. That’s why it is combined with `STANDALONE` env which should be `true` when a remote app is run as its standalone version. Otherwise shared modules won’t be available in the initial bundle and the app is likely to crash. 

As you can see on the code snippets above, all the plugins’ configs, except the host app, have `exposes` property. This is a list of modules that the application will export as remote to another application. The host app can only import remote modules and cannot have pre-defined ‘exposes’ or ‘remotes’ properties in Webpack config. However, MFEs can do both, expose and import remote modules at the same time. 

While ‘exposes’ and ‘remotes’ properties are sufficient to support Module Federation in web apps, it’s not enough in a mobile environment. For Re.Pack, we have to add one more thing to handle fetched scripts from remote - a resolver. That’s why the `ScriptManager` was introduced in Re.Pack 3.

This is a manager that eases the resolution, downloading, and executing additional code from:

- arbitrary JavaScript scripts
- Webpack chunks
- Webpack bundles
- Webpack MF containers

To make `ScriptManager` resolve our remote scripts properly, we need to pass URLs to remote locations that those scripts will be available to fetch. They should be passed in proper object shape accepted by the createURLResolver method from the Federated module. In a very basic example, URLs might be hardcoded and the implementation is presented in the snippet below:

```
const resolveURL = Federated.createURLResolver({
  containers: {
    app1: 'http://localhost:9000/[name][ext]',
    app2: 'http://localhost:9001/[name][ext]',
    module1: 'http://localhost:9002/[name][ext]',
  },
});

ScriptManager.shared.addResolver(async (scriptId, caller) => {
  let url;
  if (caller === 'main') {
    url = Script.getDevServerURL(scriptId);
  } else {
    url = resolveURL(scriptId, caller);
  }

  if (!url) {
    return undefined;
  }

  return {
    url,
    cache: false,
    query: {
      platform: Platform.OS,
    },
  };
});
```
[ScriptManager usage inside host application.ts](https://gist.github.com/callstack-bot/22ac442379284e7acd1473965c38f579#file-scriptmanager-usage-inside-host-application-ts) hosted with ❤ by GitHub

_Code snippet 4. ScriptManager usage inside the host application_

However, there is an option to provide remote containers URLs dynamically. That means removing hardcoded URLs and fetching them from external service right in `addResolver`. Implementing the external service is optional but might become very useful on a larger scale. It allows to manage remote containers' URLs from outside of the host app and to switch them without the need for the host app re-deployement. Furthermore, it might help to handle fetching theproper version of MFEs inside the host app when a version management system is required. 

```
ScriptManager.shared.addResolver(async (scriptId, caller) => {
  let url;
	const containers = await fetchURLs();
	const resolveURL = Federated.createURLResolver({
	  containers,
	});

  if (caller === 'main') {
    url = Script.getDevServerURL(scriptId);
  } else {
    url = resolveURL(scriptId, caller);
  }

  if (!url) {
    return undefined;
  }

  return {
    url,
    cache: false,
    query: {
      platform: Platform.OS,
    },
  };
});
```
[ScriptManager usage with dynamic URLs.js](https://gist.github.com/callstack-bot/1267869dff1de6fdb57507d4f52e815d#file-scriptmanager-usage-with-dynamic-urls-js) hosted with ❤ by GitHub

_Code snippet 4.1. ScriptManager usage with dynamic URLs_

After setting up the `ScriptManager`, we can use Module Federation functionality in the code. Here is the `App.tsx` file from the host application:

```
// eslint-disable-next-line import/no-extraneous-dependencies
import { Federated } from '@callstack/repack/client';
import React from 'react';
import { Text, SafeAreaView } from 'react-native';

const App1 = React.lazy(() => Federated.importModule('app1', './App'));
const App2 = React.lazy(() => Federated.importModule('app2', './App'));

export default function App() {
  return (
    <SafeAreaView>
      <Text>Host App</Text>
      <React.Suspense fallback={<Text>Loading app1...</Text>}>
        <App1 />
      </React.Suspense>
      <React.Suspense fallback={<Text>Loading app2...</Text>}>
        <App2 />
      </React.Suspense>
    </SafeAreaView>
  );
}

```
[App.tsx code from host application.tsx](https://gist.github.com/callstack-bot/538d0afc7e037e144a1693bed49a9ba7#file-app-tsx-code-from-host-application-tsx) hosted with ❤ by GitHub

The imports are async, so we should wait till the script will be downloaded, resolved by `ScriptManager`, and ready to use. So, we added `React.lazy` and `React.Suspense` syntax for handling async imports. React.Suspense has the fallback property that allows showing loader component to the users during loading scripts. To dynamically import remote modules in the host app, we also used the utility function `importModule` from `Federated` namespace, to provide the correct container and module name.

But, let’s take a look at the previously presented code from code snippet 2. There is remotes property under the Module Federation plugin in Webpack config. It helps to use the same syntax as for static imports inside app1 when importing modules from remote containers. Also, it’s worth mentioning that the component name needs to be prefixed with the remote container name. In this case, it’s module1/Root. According to what was written before - the remotes property can be used only in MFEs but anyway, it should still use `React.Suspense` when rendering remote containers:

```
import * as React from 'react';
import { Text } from 'react-native';

// eslint-disable-next-line import/no-unresolved
import Module1 from 'module1/Root';
import { foo } from './foo';

export default function App() {
  const [fooText, setFooText] = React.useState<string>('');

  React.useEffect(() => {
    (async () => {
      try {
        const fooText = await foo();
        setFooText(fooText);
      } catch {
        setFooText('Failed to get foo text');
      }
    })();
  }, []);

  return (
    <>
      <Text>App 1</Text>
      <React.Suspense fallback={<Text>Loading module1...</Text>}>
        <Module1 />
      </React.Suspense>
      <Text>{fooText}</Text>
    </>
  );
}
```
[App.tsx code from app1 application.tsx](https://gist.github.com/callstack-bot/25df7f90ea4f7d7083487128c94450ba#file-app-tsx-code-from-app1-application-tsx) hosted with ❤ by GitHub

_Code snippet 6. App.tsx code from app1 application_

In code snippet 3 there is also a defined remotes property that allows importing modules exposed by app1. This pattern is called bi-directional import. It won’t be possible outside the Module Federation when importing module B in module A and module A in module B at the same time because of circular dependencies. However, Module Federation allows it and it’s totally fine until both modules are imported from different MFEs. The only thing that developers have to keep in mind is that it might be easy to end up with an endless loop e.g. by calling one function by another as presented in the code snippet below:

```
let counter = 3;

export async function baz() {
  if (counter <= 0) {
    return 'end';
  }

  // eslint-disable-next-line import/no-unresolved
  const { foo } = await import('app1/foo');

  counter--;
  return `baz + ${await foo()}`;
}
```
[baz function inside module1 which uses foo function from app1.tsx](https://gist.github.com/callstack-bot/37d9f1ff532e1166457fbc1440452c89#file-baz-function-inside-module1-which-uses-foo-function-from-app1-tsx) hosted with ❤ by GitHub

_Code snippet 7. baz function inside module1 which uses foo function from app1_

Please notice that the foo function from app1 uses baz function from module1. So as mentioned above, circular dependencies are working perfectly fine:

```
// eslint-disable-next-line import/no-unresolved
import { baz } from 'module1/baz';

export async function foo() {
  return `foo + ${await baz()}`;
}
```
[foo function from app1.tsx](https://gist.github.com/callstack-bot/0bef51519142d66c15bb7b2bec736cff#file-foo-function-from-app1-tsx) hosted with ❤ by GitHub

_Code snippet 8. foo function from app1_

On code snippet 7, there is a counter variable that prevents an endless loop caused by calling `foo` by `baz` and `baz` by `foo`.

## Summary
Module Federation is a powerful feature implemented in Webpack 5 and carefully migrated to Re.Pack 3 package.

Thanks to its functionalities, Module Federation can be a great ally for large development teams working on complex products, like super apps. The main benefits of Module Federation are

- Independent deployment. No need to re-deploy host application on remote containers changes and re-deployment
- Separating huge development teams. As every container could be deployed independently each team could work on their container 
- Sharing the code between tightly loosely coupled applications

_This article was originally published at [callstack.com](https://www.callstack.com/blog/module-federation-with-re-pack-3) on October 4, 2022._