A Step-By-Step Guide to Super App Development
This article explores the development of super apps, emphasizing a tech perspective with a focus on building a super app from scratch and migrating an existing solution. It delves into the use of Re.Pack and Module Federation for creating super apps and addresses challenges like aligning dependencies, handling assets, and sharing state within a super app setup.
With the number of active mobile users growing and the consumers' demands for smooth mobile-first experiences strengthening each year, it’s no surprise you’re considering jumping on the super app bandwagon. After all, super apps come with a number of benefits for both users and the businesses behind them.
To help you take the first step, we’ve put together some tips for developing a super app from the tech perspective. As businesses and their products come in all shapes and sizes, we’re focusing here on two scenarios:
- building a super app from scratch,
- migrating your solutions to a super app setup.
Speaking of different shapes and sizes, when creating a super app, you can choose from a couple of different approaches, including:
- Native Android application with Feature Delivery
- Native iOS application with WebViews
- Cross-platform React Native application with Metro
- Cross-platform React Native application with Webpack and Re.Pack
In this article, we’ll be focusing on the latter, not only because we’re the people behind Re.Pack, but primarily because this solution allows you to enjoy the most benefits compared to other tools. Building your super app with Re.Pack means the ability to reuse features across apps, smaller JS bundle size, OTA updates, time and cost-effective development experience, and potential to leverage third-party contributors – these perks pretty much speak for themselves.
Now let’s get down to the super app development business, shall we?
Building a super app from scratch
For the sake of this tutorial, we’ve prepared an example repository with two apps inside one monorepo, namely <rte-code>HostApp<rte-code> and <rte-code>MiniApp<rte-code>. You can find the repository with the example here.
The <rte-code>HostApp<rte-code> is a simple app with <rte-code>HomeScreen<rte-code> and a <rte-code>DetailScreen<rte-code>, which you can reach via a button on the <rte-code>HomeScreen<rte-code>. <rte-code>MiniApp<rte-code> is similar, but has a button that navigates to the <rte-code>GalleryScreen<rte-code> with a list of pictures.
For now, those are two separate apps that have nothing in common with each other and run independently. Each app has its own navigation with a few screens. Our goal in this part will be to bring the <rte-code>MiniApp<rte-code> into the <rte-code>HostApp<rte-code> using Re.Pack and Module Federation.
Why this combination of technologies? Re.Pack is an Open Source toolkit that allows you to build React Native apps with the support of the Webpack ecosystem. Thanks to supporting Module Federation ever since the 3.0 version, it’s particularly fit for developing mobile applications that benefit from code splitting – and super apps are a great example of those.
Now that you get the gist of using Re.Pack and Module Federation to build a super app, we can get to the nitty-gritty. Let’s get started by cloning the repository:
Setting up Re.Pack in both HostApp and MiniApp
In both <rte-code>packages/host-app<rte-code> and <rte-code>packages/mini-app<rte-code> install Re.Pack with its dependencies:
Make sure to run <rte-code>yarn bootstrap<rte-code> from the repository's root to update the pods that come with those dependencies.
Now let’s make some adjustments to the setup. To make React Native CLI able to use Re.Pack commands (such as <rte-code>webpack-start<rte-code> or <rte-code>webpack-bundle<rte-code>), we will add the following content to <rte-code>react-native.config.js<rte-code> (or create it if it doesn't exist):
Having done that, let’s update our <rte-code>package.json<rte-code> scripts to reflect these changes. Edit the <rte-code>start<rte-code> script for both apps, so it utilizes new <rte-code>webpack-start<rte-code> command, which starts the development server for Re.Pack:
Now add <rte-code>webpack.config.mjs<rte-code> to each app in our monorepo. For now, we’ll start with a default template that will be modified along the way. You can find the ES Module template here or the CommonJS equivalent here. Either one will work just fine.
The last step of this part is to modify the scripts used for bundling the iOS and Android.
Open your application's XCode project/workspace and:
- Click on the project in the Project navigator panel on the left
- Go to Build Phases tab
- Expand Bundle React Native code and images phase
- Add export BUNDLE_COMMAND=webpack-bundle to the phase
Go to <rte-code>android/app/build.gradle<rte-code> in both apps, uncomment the line with <rte-code>bundleCommand<rte-code> in the react block, and modify it to use <rte-code>webpack-bundle<rte-code>:
That’s the basic setup for Re.Pack; try running the apps to see if everything works as expected.
Setting up ModuleFederationPlugin
Now it’s time to set up the <rte-code>ModuleFederationPlugin<rte-code>. Not long ago, you were required to do some hacky workarounds to get the Module Federation working with Re.Pack. Since the release of version 3.0, though, the <rte-code>ModuleFederationPlugin<rte-code> has been included out of the box.
Let’s add a new instance of <rte-code>RePack.ModuleFederationPlugin<rte-code> to our <rte-code>webpack.config<rte-code> in the <rte-code>HostApp<rte-code>:
We’ve added all our dependencies because they all have native code inside, and whenever a dependency has native parts, you must add it to the shared field inside <rte-code>ModuleFederationPlugin<rte-code>.
We’ve also specified <rte-code>singleton<rte-code> and <rte-code>eager<rte-code> on them.
<rte-code>Singleton<rte-code> means that only one instance of such a module will ever be initialized, which is a requirement for React and React Native. It is usually a good idea to make the dependencies with native parts <rte-code>singleton<rte-code> as well.
<rte-code>Eager<rte-code>, on the other hand, means that the module is added to the initial bundle and will not be loaded later. All shared modules in the host app should be <rte-code>eager<rte-code>. In remote containers, it depends on the build configuration. When you want to run the app as a standalone, you need to mark all the shared dependencies as <rte-code>eager<rte-code> as well.
Having that in mind, let's add a similar configuration of the plugin to the mini app:
1. Inside webpack.config.mjs, let’s grab <rte-code>STANDALONE<rte-code> from the process.env
2. Now let’s add the <rte-code>ModuleFederationPlugin<rte-code> just like in the <rte-code>HostApp<rte-code>
3. Finally, let’s add a separate script to run webpack in our mini app in standalone mode and modify the previous start script to run on a different port (which will come in handy soon when we need to have both our development servers running at the same time)
<rte-code>STANDALONE env<rte-code> variable will help us to distinguish when we want to run the app as a separate entity or as a mini app. Currently, modules specified in <rte-code>shared<rte-code> will be eagerly loaded only when running the app on its own.
Setting up ScriptManager in the HostApp
The last part of the setup to get the <rte-code>HostApp<rte-code> ready to accept remote containers is to add the <rte-code>ScriptManager<rte-code>’s resolver. We need to tell our app where to find the mini app, and we’re about to do it now.
Let’s add the following piece in the <rte-code>HostApp’s index.js<rte-code> on line 4
As <rte-code>ScriptManager<rte-code> can have many resolvers, we need to return the configuration for the bundle only if we get a URL match. Implicitly returning undefined here means the <rte-code>ScriptManager<rte-code> should keep looking at other resolvers to find a proper match. In the example, the configuration contains info about the URL and platform, but many more options are available for the more advanced use cases.
Our basic Module Federation setup is now complete, and we can start connecting the two apps.
Using MainNavigator from the MiniApp in the HostApp
In this part, we will be going through the process of reusing the whole <rte-code>MiniApp<rte-code> inside our <rte-code>HostApp<rte-code>. The part we will need is the <rte-code>MainNavigator<rte-code> from the <rte-code>MiniApp<rte-code>, as we don’t want to have two <rte-code>NavigationContainers<rte-code> in our app.
First, we must expose the <rte-code>MainNavigator<rte-code> from the <rte-code>MiniApp<rte-code> to be available for consumption in the <rte-code>HostApp<rte-code>. Let’s go to MiniApp’s <rte-code>webpack.config.mjs<rte-code> and expose it there:
You can expose the components to be consumed by adding them in the form of key-value pairs, where the key is the name used to refer to that component in the <rte-code>HostApp<rte-code>, and the value is the path to the component. Note that for this to work, we must use <rte-code>export default<rte-code> in our component.
Once we’re done with that, it’s time to import the <rte-code>MiniApp<rte-code> into the <rte-code>HostApp<rte-code>. Let’s create a new screen that will house the <rte-code>MainNavigator<rte-code> from the <rte-code>MiniApp<rte-code>:
<rte-code>Federated.importModule<rte-code> takes two parameters: the name of the container and the name of the component that we want to import. As we want to load the component only when the user enters that particular screen, we need to put the import inside <rte-code>React.lazy<rte-code> and render it within <rte-code>React.Suspense<rte-code>.
To finish up, let’s add the <rte-code>MiniAppScreen<rte-code> to the <rte-code>MainNavigator<rte-code> in the <rte-code>HostApp<rte-code> and add a button to navigate to <rte-code>MiniAppScreen<rte-code> inside <rte-code>HomeScreen<rte-code>:
Now we should have a working <rte-code>MiniApp<rte-code> inside our <rte-code>HostApp<rte-code>. Go ahead and start both development servers using <rte-code>yarn start<rte-code> command in the root of the repository, and then let’s run the host app. If you choose to run the app on Android, remember to expose the port on the Android emulator to your machine by running:
Click on the newly added button to Navigate to MiniApp and enjoy your super app!
It’s easy to notice that after navigating to the <rte-code>MiniApp<rte-code>, we end up with two navigation headers. We can fix that by adjusting the header in the <rte-code>HostApp<rte-code> or exposing a navigator without the header; the decision is up to you.
Migrating to a super app setup
Migrating to a super app setup requires careful consideration and planning. Upgrading your existing setup with Re.Pack and Module Federation, as we’ve described above, shouldn’t pose problems in itself. However, it may become challenging in the context of your current project and the custom solutions it involves. In this part, we look at such challenges and cover a few key things to consider when turning your product into a super app.
If you’re considering enriching your product portfolio with a super app, your codebase is probably already large enough to contain many dependencies. The biggest challenge lies in aligning the dependencies between respective apps, so they can be shared as efficiently as possible.
If there is an issue with one library or component, the federated module could suffer misaligned versions of libraries in the host app. Some tolerance is involved, as Re.Pack will try to fetch the missing dependencies by default, and runtime errors might not be the case. At the end of the day, however, Re.Pack will complain about version requirements not being met, albeit in development mode.
When the package contains native code and the versions are not aligned, a runtime error will occur. To avoid that, it is a good idea to have an error boundary wrapper that prevents federated modules from crashing your host app and displays fallback components when such issues arise.
Another thing that’s vital to successfully migrating your app is handling the assets. When the mini app is embedded into the host app, it won’t be able to access its local assets, as they won’t be added to the host app asset’s registry in build time. You need to remember that this problem won’t manifest itself in development mode with dev-server – only when working with a static bundle produced by the <rte-code>react-native webpack-bundle<rte-code> command.
Re.Pack offers two ways to deal with that. The first one, and by far the preferred one, is to handle the assets just like we do on the web, where our assets are first uploaded to a remote server (preferably CDN) first, and only then can be downloaded by the users. Re.Pack provides allows you to gather all the assets and modify the final bundle so that the production build can download them on demand.
These assets are now remote, so it might be necessary to include a placeholder to display while loading them. When working with remote assets, the only limitation is that the hostname of your CDN needs to be known at build time, so Re.Pack can generate the URLs for these assets.
Another alternative to consider is inlining all the assets from the federated module, so they are downloaded with the bundle itself. You can do that by adjusting the <rte-code>assetLoader<rte-code>’s options in the <rte-code>webpack.config.mjs<rte-code>, namely: setting inline to true. This solution has one major drawback: it significantly increases the bundle size. You might consider inlining some assets if you don’t want them to be publicly accessible on the web, or they are vital to a swift startup of your mini app, as they will be instantly visible to the user.
Sharing state across the app
When trying to create a super app from an existing app by extracting a certain functionality to a mini app, state management might become an issue. Until now, the state has been tightly coupled with the whole app. If you want your mini app to be a consumer of that state, it is necessary to address that problem.
For the sake of this example, let’s assume we have a setup like in the first part of the article. To make our state management solution reusable across the board with mini apps and the host app, we could extract the shared state into a separate package. Depending on whether you would like to update the state part of the app over-the-air, you could make it into a federated module as well. Otherwise, this can be a statically imported module into both the host app and the mini app.
If we specify this module inside the <rte-code>shared<rte-code> portion of the Re.Pack’s <rte-code>ModuleFederationPlugin<rte-code>, we will only have one copy of that module during the runtime of our super app. If that’s the case, we will be able to place the <rte-code>ContextProvider<rte-code> component in the host app and access the same instance in our mini app, having a shared state across the whole app.
These are just a few examples of challenges you might face when migrating to a super app setup. It’s hard to tell what you might run into, as every app is unique in its own way. Mind that Module Federation has been present in the web environment for quite some time, so if your problem is strictly related to the JS part of the app, chances are someone solved a similar problem already, and the concept might be applicable in the context of super apps.
We hope this article has shed more light on what it takes to develop a super app from the tech standpoint. Whether you’re up for building a super app from scratch or migrating your current solution into such a setup, it might turn out that your team needs some help to do it right. That’s why we’ve prepared a super-app-showcase. Use it to your advantage, and don’t hesitate to reach out to us if you have any questions about super app development.
If you’re looking for more organizational tips, we’ve also got you covered with a blog post about developer experience and planning the work of your in-house team and third-party contributors. We’ve also published a super-app-showcase case study that might significantly speed up your work.