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.importModule
To load dynamic containers you can use Federated.importModule
and add a resolver for it and it's chunks, for example:
remotes
Another 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
remotes
This 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 singleton
remotes
eager
and singleton
React 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
:
remotes
Currently, 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
.