How to Use Module Federation With Re.Pack 3

Authors
Andrew Andilevko
Software Engineer
@
Callstack

Along with the release of 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. 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 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.

module federation in the app
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',
},
},
})

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',
},
})

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',
},
})

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,
},
};
});

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 addResolve. 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,
},
};
});

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

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>
</>
);
}

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

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

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

Insights

Learn more about Super Apps

cover

Aug 31Article

Working With the React Native Super App Showcase Repository

Explore the React Native Super App Showcase architecture: Re.Pack, Module Federation, runtime bundles, shared dependencies, and production releases.

cover

Jul 21Article

A Step-By-Step Guide to Super App Development With Re.Pack 5

Learn how to connect a React Native host and mini app with Re.Pack 5 and Module Federation, then load the remote module at runtime.

cover

Jul 2Article

Mobile Module Federation with Re.Pack: When Runtime Delivery Is Worth the Complexity

Learn how Module Federation works in React Native with Re.Pack, when it makes sense, when simpler options are better, and what teams need to validate before adopting runtime delivery.

cover

May 19Article

Using TurboModule Substitution to Build Safer React Native Plugin Systems

Learn how TurboModule substitution helps React Native plugin systems provide native capabilities while preserving sandbox boundaries.

cover

Aug 27Article

Re.Pack 5.2: Faster Babel Transforms, RN 0.80 & 0.81, Rozenite

This release focuses on parallel, smarter transforms with a new Babel & SWC loader, official support for React Native 0.80 and 0.81, fully typed configs, support for Rozenite and more

cover

Jul 31Article

Running Multiple Instances of React Native in Sandbox

react-native-sandbox lets you run multiple React Native instances in one app. Build secure super apps and plugins with true runtime isolation, crash containment, and configurable permissions for native modules, perfect for integrating third-party code safely.

Event Cover

May 21Talk

Building Universal Super Apps With React & React Native

Discover how to build universal Super Apps using React and React Native. Join Mike Grabowski, CTO at Callstack, as he explores dynamic code delivery, native UI rendering, and how RePack bridges the gap with Module Federation.

Event Cover

May 20Live Stream

What’s New in Re.Pack 5.1: Live Overview

Explore runtime hooks, prefetching MF remotes, and more in Re.Pack 5.1. Join us live or watch the replay.

cover

Jan 29Podcast

Mobile Microfrontends With Zephyr Cloud and Re.Pack

Get ready for faster development cycles and reduced risks when rolling out updates for mobile apps with microfrontends enabled by Zephyr Cloud and Re.Pack.

Event Cover

Jul 8Webinar

How to Scale Teams, Product Development, and Release Process With Re.Pack

Join us to learn how Re.Pack helps manage complexity in React Native apps, enables scalable teams, and boosts release processes.

cover

Nov 7Podcast

Module Federation

In the 18th episode of the React Native Show Podcast, Łukasz Chludziński, Paweł Trysła, and Zack Jackson talk about Module Federation in Webpack and Re.Pack. They also discuss some of the buzzwords related to this type of architecture, for example, super apps and micro frontends.

cover

Jul 6Podcast

Re.Pack - Bringing Webpack to React Native

This episode explores Re.Pack, an Open Source, Webpack-based toolkit to build React Native apps with the full support of the Webpack ecosystem.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.