Before diving deep into Module Federation, it's important to understand how Code Splitting works in React Native with Re.Pack and what are the challenges.
Module Federation is similar to Code Splitting, but offers more control, flexibility and scalability.
We highly recommend to read and understand Code Splitting first, before trying Module Federation:
Module Federation support in Re.Pack is still at early stages. We believe it should work for many cases, but if there's a use-case which we don't support, don't hesitate to reach out and ask about it.
Module Federation is an architecture, which splits the application into multiple pieces. These pieces are called containers. Similarly to micro-services, Module Federation splits application into a distributed frontends, sometimes referred to as micro-frontends.
The main benefits or Module Federation are:
Keep in mind that this list is not exhaustive. It's possible you could benefit from Module Federation in another way as well.
Not every project or application is a good fit for Module Federation. Due to nature of Module Federation there's are few challenges and overheads you need to consider:
We always recommend to create a prototype or a Proof-of-Concept application, to better understand the challenges and forsee potential problems and effort needed to adopt Module Federation.
Here's a list of currently know limitations:
eager and a singleton.You should also consider limitations and T&C of the store you would be deploying the application to. You can read more on Code Splitting page - the same limitations and caveats apply.
You can explore Module Federation example of React Native application using Re.Pack here: https://github.com/callstack/repack-examples/tree/main/module-federation.
There are multiple resources available for you about Module Federation. They are specific to Web, but the same ideas apply when adopting Module Federation in React Native.
We suggest to go through the links below to get familiar with Module Federation for Web and then come back and compare differences between Module Federation on Web and in React Native:
Before adopting Module Federation in React Native, we recommend to create a Web-based prototype and then, integrate it into a React Native project with React Native specifics.
Re.Pack provides custom Module Federation plugin - Repack.plugin.ModuleFederationPlugin.
It's a recommended way to use Module Federation with Re.Pack. It provides defaults for filename, library, shared and converts remotes into promise new Promise loaders with Federated.createRemote function automatically.
For example a host config could look similar to:
And containers:
In Module Federation with Re.Pack you can choose if you want to have containers loaded statically, dynamically or both.
Federated.importModuleTo load dynamic containers you can use Federated.importModule and add a resolver for it and it's chunks, for example:
remotesAnother way to load container is with remotes. You specify what containers will be used in remotes, but the URL resolution will be dynamic. Using remotes allows you to import containers using standard import statement (import ... from '...';).
In the code it could look similar to:
And the remotes have to be configured inside Repack.plugin.ModuleFederationPlugin:
Keep in mind, remotes cannot be used inside Host application: Host application can't use remotes
remotesThis options is similar to Semi-Dynamic containers with remotes but doesn't require to manually provide resolver with ScriptManager.shared.addResolver. Instead, the URL for resolution is specified at build time inside remotes:
This will add a default resolver based on the URL after @, so you can import federated module without calling ScriptManager.shared.addResolver:
Keep in mind, remotes cannot be used inside Host application: Host application can't use remotes
ScriptManager's resolvers in Module FederationIn Module Federation setup, ScriptManager can be used in a similar way as you would use it with standard Code Splitting.
The main difference is with resolvers:
remotes and provide URLs in plugin configuration (eg module1@https://example.com/module1.container.bundle) - this would add a default resolver for container module1 and it's chunks.ScriptManager.shared.addResolver or
a host application can provide resolver for containers.src_App_js chunk for container app1 and a resolver for src_App_js for container app2.Relevant only when using dynamic/semi-dynamic containers.
When using a single resolver in the host, we recommend to use Federated.createURLResolver to reduce boilerplate:
The example above would resolve chunks and container according to the table below:
scriptId |
caller |
url |
Notes |
|---|---|---|---|
'src_App_js' |
'main' |
'https://somewhere3.com/src_App_js.chunk.bundle' |
Chunk of Host application |
'src_Body_js' |
'main' |
'https://somewhere3.com/src_Body_js.chunk.bundle' |
Chunk of Host application |
'app1' |
undefined |
'https://somewhere1.com/app1.container.bundle' |
Container entry |
'src_App_js' |
'app1' |
'https://somewhere1.com/src_App_js.chunk.bundle' |
Chunk of container 'app1' |
'app2' |
undefined |
'https://somewhere2.com/app2.container.js' |
Container entry |
'src_App_js' |
'app2' |
'https://somewhere2.com/chunks/src_App_js.chunk.bundle' |
Chunk of container 'app2' |
Relevant only when using dynamic/semi-dynamic containers.
With multiple resolvers you can call ScriptManager.shared.addResolver multiple times in the Host application or have a dedicated resolver per container:
In React Native project with Module Federation, there has to be a Host application, also known as Shell.
A Host application is a React Native application, which is usually released to the stores as a final product, delivered to the customers/users.
There can be multiple host applications in single project, but each of these hosts must meet the following requirements:
eager and singletonremoteseager and singletonReact Native requires a single instance of react and react-native dependency, otherwise the application crashes. On Web, usually you want to have react and react-dom shared, but they don't have to be eager.
The reason why react and react-native have to be eager in React Native is because the JavaScript context in React Native has to be initialized - the logic that sets up the environment lives inside react-native's InitializeCore.js.
The initialization must be done as a first step and it has to be done synchronously before AppRegistry.registerComponent() is called.
In practice, this means that react and react-native must be configured inside shared as both eager and a singleton in all containers:
import('./bootstrap') is not supportedIn many guides and tutorials, you will find import('./bootstrap') inside an entry file to an application (usually index.{js,ts}). This dynamic import, creates a async boundary and allows react/react-dom to be lazy and
it's a recommended way to deal with the Uncaught Error: Shared module is not available for eager consumption error (outlined in https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption).
This works for Web, because DOM API provides functionalities to load and execute additional JavaScript code out of the box.
However, React Native doesn't provide any APIs to load additional code by default. The only way to execute additional code is to use native module to load it and evaluate on the native side. But, in order to use native modules, the bridge between JavaScript and native has to be established, which happens when React Native initializes the environment. In order words, only after React Native is initialized, it's possible to load and execute additional JavaScript code, which happens through ScriptManager.
In practice, this means that your entry code should look similar to the following snippet:
This code can be place inside entry <projectRoot>/index.js, but we recommend to put it inside <projectRoot>/src/bootstrap.{js,ts} and use a synchronous import statement inside <projectRoot>/index.js:
remotesCurrently, there's a limitation for Host application preventing them from using remotes in Repack.plugins.ModuleFederationPlugin.
In order to load a container from the host, you have to use Federated.importModule:
The code above, will load app container, import module App.js from it and pass it to React.lazy.
If you're planning on using native modules, the host application must provide native code for those. It's also recommended to make those modules shared and a singleton.
For example, if you want to use react-native-reanimated, you must add it to the host all all the containers you want to use Reanimated in, then configure Repack.plugins.ModuleFederationPlugin in host and the containers using the dependency:
remotes must use Federated.createRemote(...) functionBy using Repack.plugins.ModuleFederationPlugin, remotes will be automatically converted to promise new Promise using Federated.createRemote function.
Only relevant when not using webpack.container.ModuleFederationPlugin instead of Repack.plugins.ModuleFederationPlugin.
ScriptManager, which allows to load and evaluate additional JavaScript code (including containers), is an asynchronous API. This means the remotes in ModuleFederationPlugin must use promise new Promise(...) syntax. To avoid repetition and having to maintain promise new Promise(...) implementations yourself, Re.Pack provides an abstraction - Federated.createRemote function:
Federated.createRemote function will make the remote loadable, so you will be able to use import statement for remotes:
The loading code generated by Federated.createRemote function uses ScriptManager,
meaning you need to make sure the proper resolvers are added via ScriptManager.shared.addResolver so your remotes can be resolved, for example:
Re.Pack doesn't use public path and all chunk resolution as well as dynamic container resolution happens through resolvers added to ScriptManager via ScriptManager.shared.addResolver.