Microfrontends with React Native

Microfrontends

Speaking of microservices is more oriented to the code that is on the server side (or backend as most of us call it). On the other hand, for front-end applications we commonly use the term “microfrontends”.

Advantages

One of the main advantages of doing this, in addition to scalability, is the ability for each part of the application to be independent from the others, with its own life cycle. We can also name, as a not minor advantage, the possibility of expanding in an orderly manner the team of developers, obtaining independent responsibilities for each business area.

Applying it in React Native I (Logic)

So, with all this information, is it possible to replicate this in a mobile app developed with React Native? Yes, without a doubt. Let me tell you about our experience and the approach we applied.

The skeleton and its modules

We must think of the app in 2 parts (or actually 3, but we will talk about this third one later). On one hand, we will have a base repository or skeleton and, on the other hand, the microfrontends or modules. The base repository will be in charge of importing the modules at runtime with the app already assembled. Therefore, the modules will be in charge of providing the specific business area to which each one is dedicated, providing a different and unique functionality.

Skeleton or App

Whatever we call it, the only thing that really matters is its function. This skeleton is going to be in charge of importing and assembling the microfrontends to arrange the whole app. But why is it called skeleton? Easy: because it basically has very little code and logic. Here we only import modules and configure the app. This is what we call the entry point of the app; the only file you must have is the index.js with imports. Because of how modules are built and how react works, this repository is where, if necessary, the native code of the respective phone OS (iOS/Android) will be modified and/or added.

Microfrontends or Modules or Packages

The way to make a microfrontend is simple. It must have the needs of a specific business area and depends on anyone. From the beginning to the end of the life cycle, what you want to provide must be completely contained within the package. This is why we conceptually call it a “stand-alone functionality module”.

Third part or Engine

Above we said that the app was divided into 2, but there was actually a third part. Let’s see what this is all about.

  • Libraries will be installed only once in the whole app.
  • As it is a package loaded in the package manager, it will be accessible from any other module.
  • It allows the reusability of many things, since without this package the other modules would be repeating code in their repositories because they do not know each other.
  • Development in local environment is also solved, each repository is managed separately, it should not be embedded in the base repository and with a simple configuration file on the javascript bundler the imports are solved.

Applying it in React Native II (Practical)

We already have the logic of how to do it, now let’s see how we can develop it.

Let’s get to work with Bob the builder (literally).

To create the module we can use the CLI create-react-native-library which initializes a repository already configured and ready to use with a library called react-native-builder-bob, which will be in charge of compiling and building our package before sending it to the package manager we are using. In my personal opinion, there are some things that are not necessary in the repository created, and that is why I leave this link so you can see how we create it ready to start using.

  • "module": "lib/module"→ will be the property used by babel to compile the files when generating executables.
  • "source": "src" → will be the property used by babel and metro to compile the files at runtime and achieve hot reload in a local environment.

Configuring imports and developing locally

In our code we have differents possibilities to import the elements:

import * as example from 'example';
import example from 'example';
import {example} from 'example';
import example, {example2} from '@example/path/to/something';
const DEV_MODE = true;const getBabelAlias = () => {
const path = require('path');
const pak = require('@aagusriva/microfrontends-with-react-native/package.json');
if (DEV_MODE) {
return {
[pak.name]: path.join(
'..',
'microfrontends-with-react-native',
pak.source,
),
};
} else {
return {
[pak.name]: path.join(pak.name, pak.module),
[pak2.name]: path.join(pak2.name, pak2.module),
};
}
};
module.exports = {
presets: ['module:metro-react-native-babel-preset', '@babel/preset-flow'],
plugins: [
[
'module-resolver',
{
extensions: [
'.ios.js',
'.android.js',
'.tsx',
'.ts',
'.js',
'.json',
],
alias: getBabelAlias(),
},
],
],
};
const DEV_MODE = true;const getMetroLocalConfig = () => {
if (!DEV_MODE) {
return {};
}
const path = require("path");
const blacklist = require("metro-config/src/defaults/exclusionList");
const escape = require("escape-string-regexp");
const pak = require("@aagusriva/microfrontends-with-react-native/package.json");
const pakRoot = path.resolve(
__dirname,
"..",
"microfrontends-with-react-native"
);
const modules = Object.keys({
...pak.peerDependencies,
});
const modRoute = modules.map((m) => path.join(pakRoot, "node_modules", m));
return {
projectRoot: __dirname,
watchFolders: [pakRoot, pak2Root],
resolver: {
blacklistRE: blacklist(
modRoute.map((m) => new RegExp(`^${escape(m)}\\/.*$`))
),
extraNodeModules: modules.reduce((acc, name) => {
acc[name] = path.join(__dirname, "node_modules", name);
return acc;
}, {}),
},
};
};
module.exports = {
...getMetroLocalConfig(),
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
};

Global status management

I could not close the article without giving a minimal introduction on how we handle the global state of the whole application. In our case we use redux.

  1. Import the main router of each module → Import the main store of each module.
  2. Create the general router of the whole app → Create the global store of the whole app.
export * from 'react-redux';
export * from 'redux';
import { createStore, combineReducers } from '@example-app/engine-module/packages/redux';
import reducerModule1 from '@example-app/module-1/store/reducers';
import reducerModule2 from '@example-app/module-2/store/reducers';
const reducers = combineReducers({ reducerModule1, reducerModule2 });
const store = createStore(reducers);
export default store;

Results

Once this architecture is applied in an app, several positive things can be seen:

  • High cohesion and low coupling (one of the fundamental pillars of software design) as each module performs its own task without getting involved in the others.
  • The order we achieve in the code, since we have a much cleaner flow.
  • Possibility to expand to really large numbers of development teams where each one could, for example, be in charge of a specific business area
  • The code remains the same for all environments
  • By publishing each version when it is completely finished, you can generate an executable at any time, regardless of the ongoing tasks of the other teams, since you will be using the published version.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Agustin Riva

Agustin Riva

Software Engineer, mostly frontend developer :)