Skip to content
Logo Theodo

Microfrontends in Mobile with React Native

Mo Khazali10 min read

A single Monolith being split into smaller services

Introduction

This article is meant to be an exploration of the state of creating Microfrontends (MFEs) for native mobile apps in 2023. It covers the history of MFEs, giving a brief intro, and then looks into how React Native’s architecture can theoretically make MFEs possible. We look at a naive implementation of MFEs in a brownfield app, using CodePush to handle over-the-air (OTA) updates. Finally, we investigate what solution already exists, some of the challenges around MFEs in mobile, and what we can pragmatically achieve today.

This article is not making a case for using MFEs in general, since the architecture is only suitable for large scale organisations that have several teams and are facing very specific scaling issues.

From Monoliths to Microservices to Microfrontends

The story of microfrontends, ironically, starts from the backend. In the early 2000s, people were starting to get sick of having these huge monolithic backends that would contain everything under one umbrella. Having massive backend apps meant that both the app and also the development teams building the app were slowed down. The cognitive load to work on a monolithic backend that may have business logic for multiple features (including ones that you may never touch or interact with) was massive.

Naturally, teams started looking at breaking these down into smaller isolated “services” that could each contain their own logic. If they needed to communicate with other “services”, they could do so via APIs or some type of event bus.

A diagram breaking down a monolith into Microservices

Hence, the microservices architecture was born.

Fast forward around a decade - our applications have shifted more and more logic into the frontend, and similar scaling problems started to surface for large scale projects that had multiple teams working on them. Hence, the concept of Microfrontends was born for the web:

“An architectural style where independently deliverable frontend applications are composed into a greater whole”

If you’re new to the concept of MFEs, I’d highly recommend watching Luca Mezzalira’s introductory talk about them. It gives a good overview of how the architecture works.

The classic example that people look at for MFEs would be the amazon website.

An outlined screenshot of Amazon's homepage showing divisions that could be MFEs

You could break this frontend down into multiple well defined isolated bits. The header and navigation bars could each live in their own MFEs, while the promos could sit in their own MFEs.

Benefits of MFEs

Disadvantages of MFEs

Of course, the major disadvantage is the added complexity. Much like in the world of microservices, your overall app now has several different moving parts that are being managed by different teams. Effectively, this means that you have a larger surface area for failures.

On top of that, you now need a host layer that can manage all of the different MFEs and combine them together into one large app. This host layer can end up being quite complex and difficult to manage.

Microfrontends in Mobile

We can take a similar approach of breaking down apps into smaller feature based modules quite easily. This is already common practice in larger organisations that have large scale apps. If we were to imagine the Amazon app, it could be broken down into multiple “microfrontends”, each residing within their own screens/tabs.

An outlined screenshot of Amazon's mobile app showing divisions that could be MFEs

In this case, we’ve broken down the different screens into separate MFEs or modules.

This is all fine, but the concept of microfrontends falls short in the mobile space when we look at independent releases. Native mobile apps need to be fully bundled and released via an app store. This means that even if development is taking place in independent workstreams, the different modules will eventually need to be brought together and bundled before they can be shipped to users.

A diagram showing 2 modules being bundled into a single app and then deployed to the app store

The React Native Advantage

React Native can sidestep the app store deployment block. This is one of the advantages to the architecture of React Native being split into JS & Native layers. Since JS code is being interpreted at runtime, we could simply update the JS portion of the code using an over-the-air update (OTA). You can update any part of your JS code, so long as it doesn’t need access to new native layer dependencies, effectively circumventing the app store submission process for updating your app. This results in faster iterations, quicker bug fixes & releases, and ultimately, happier users (& devs).

A diagram showing React Native architecture

Having access to OTA updates is a good first step towards achieving independent release cycles.

OTA Tools

A diagram showing multiple bundles being codepushed to an app.

A popular tool to implement over the air updates is Microsoft’s CodePush. This works great for RN apps with a single bundle. However, using multiple bundles doesn’t work in CodePush, as it assumes that there is one deployment key per JS bundle. The internals of CodePush on the native layer check to see if the date of the newest bundle available on the cloud is newer than any existing bundles.


    NSString *packageFile = [CodePushPackage getCurrentPackageBundlePath:&error];
    NSURL *binaryBundleURL = [self binaryBundleURL];

	  // .....

    NSString *packageDate = [currentPackageMetadata objectForKey:BinaryBundleDateKey];
    NSString *packageAppVersion = [currentPackageMetadata objectForKey:AppVersionKey];

    if ([[CodePushUpdateUtils modifiedDateStringOfFileAtURL:binaryBundleURL] isEqualToString:packageDate] && ([CodePush isUsingTestConfiguration] ||[binaryAppVersion isEqualToString:packageAppVersion])) {
        // Return package file because it is newer than the app store binary's JS bundle
        NSURL *packageUrl = [[NSURL alloc] initFileURLWithPath:packageFile];
        isRunningBinaryVersion = NO;
        return packageUrl;
    }

This implementation has one stored binaryBundleURL for each app, and compares any new CodePush bundles with the existing bundles and packages. This means that the first bundle in the app will load correctly, but any additional bundles will break the CodePush update, giving us the ominous error message:

An update is available, but it is being ignored due to having been previously rolled back

This is because when the second bundle is opened, it tries to update the existing “bundle”, not checking to make sure these are the same bundles in the first place. Ultimately, the update fails, and we get deadlocked and our CodePush gets broken.

So, as it stands, it won’t be possible to update multiple bundles.

Can we change this?

I took some time looking at the source code, and was able to get a rough proof of concept working with a Swift app that had two separate React Native bundles:

In the most basic form, I modified the conditional above to add extra checks on the resourceName being supplied and only compare versions of the bundle if the resource names match.

There needs to be more modification on this since there’s some hash & metadata checks that are happening behind the scenes, but this was the simplest way to get a working POC.

The code I used is very WIP (and I don’t have full faith in it) so I’m working on cleaning this up and plan on opening up a PR into CodePush for this functionality.

Now even if this mechanism was perfected, there are still some issues with using multiple bundles:

This is sub ideal, albeit, it can be a potential intermediate solution for large scale native apps migrating to React Native incrementally.

Webpack for React Native?

Webpack have a very linked history with MFEs. This is largely due to the Module Federation plugin, which is the most mature implementation of MFEs. It’s a rich ecosystem that brings all the right tools to the MFE world.

In the React Native space, the Callstack team have been working on a port of Webpack that works with React Native. This brings all of the Webpack ecosystem with it, including Module Federation. This is a very cool project and opens up the door to building large scale native apps with MFEs.

Module Federation Logo with a RePack Logo

However, the flip side is that you diverge heavily from the core React Native stack by replacing Metro. This introduces a host of added complexity to a React Native project, including modified hot reloading, slower performance, and more complicated web support (you need to use Webpack directly for RN for web projects).

This becomes a larger tradeoff since the rest of the RN (and web) ecosystem seems to be moving further away from Webpack. Expo has recently announced that they no longer suggest you use Webpack for RN for web, and they suggest using Metro instead - implying that Webpack support will be dropped eventually.

In an ideal world, we can have better Async Chunk support in Metro for code splitting - and maybe perhaps some module federation-like mechanism that’s native to Metro. This is not an out-there posibility, since the Vite community have created a Vite/Rollup plugin to support Module Federation.

General Challenges in Microfrontends for Mobile

Let’s take a step back: even with native Module Federation support in Metro, there will be significant challenges to effectively using MFEs in RN apps:

What can we learn?

Recently we had a client approach us for a pretty large project - building a full scale news platform that contains several different types of media (videos, audio, and text). Each media type can have multiple layouts, and the complexity of the application required multiple teams working on it at once.

This led us to consider a feature driven approach, where we broke down the teams into vertical modularised silos. Each team would be working on a specific feature. They could run their features independently from the main app inside of a sandbox environment. This meant that each team could independently develop without stepping on each others’ feet.

Media Platform folder structure

While we’re not using full MFEs, this approach is a pragmatic way to achieve some of the benefits of splitting an application into smaller features.

I was pleasantly surprised to see a talk at RNEU this year by Sandra Jurek about Chase’s UK Mobile app architecture - they follow a similar pattern and they’ve been able to improve their development times, clearer distinctions in team ownership, and better independence of teams.

Taking these concepts of modularisation & breaking things down into packages, we’ve been able to see similar improvements and we’re excited to explore this space even further.

Feel free to reach out

Feel free to reach out to me on Twitter @mo__javad. 🙂

Liked this article?