To celebrate International Women’s Day and the upcoming release of our annual donor magazine, The Marguerite, here’s a sneak peek of one of the articles readers can look forward to… The Marguerite is…
As days, months and years go by, more and more applications are moving to the world of microservices. It is much more common to see a microservices-oriented design in modern applications than in older developments such as banks, which have been around for some time, where they still continue with monolithic applications. But what about on the front end? Or rather, what about in a mobile app?
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”.
This term has its beginnings around 2016 and refers to separating the different functionalities or business areas that a web application may have and decoupling them from themselves. What we are trying to do with microfrontends is basically to replicate the same objective that the microservice-oriented architecture has, but in the design layer or graphical interface of an application.
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.
With the advent of React, the possibility of doing this on the frontend was facilitated with the division of development into components, because if we can develop an element that is reusable throughout the entire application, why couldn’t we do the same with an entire functionality and handle it separately?
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.
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.
Modules vs Components
A very important point to highlight here is the difference between these “modules” and react components: modules are composed of navigation paths, pages, functions, states, etc. that can be reused in one or several apps. Components, however, are those parts that can be reused throughout the same section, page or application. To make it clear what I mean by module, for example in the Instagram application, one could be the home section of the app, or search section, chat, profile, etc.
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.
Finally, also from this repository app executables will be compiled, either in a local environment or in a CI/CD controlled environment.
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”.
Getting closer to what would be the code itself, a microfrontend will be built as a simple package (in our case we use npm as package manager) where entry point will be the main router (if we are using react-navigation it would be the stack navigator that the library provides us for screens management). It is important to understand that this should be the only thing to export, since it is exactly the idea of this architecture.
Another feature of these packages is that, in case of needing a dependency of an external library, it can be installed without problems, as long as it is not a library to be used in different modules. In that case we should install it elsewhere, as we will see below.
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.
Let’s think about this case:
We have a monolithic app which we want to split into microfrontends. In this app we need the external libraries react-navigation and react-redux, which are used in most of the screens, so all the modules that would come out of this app will need both libraries.
The question would be, how do we make each module use them? The quickest and easiest answer is: we install the libraries in each of these modules. But what happens with this, the first big mistake would be that we are making a bigger and heavier app since the same library would be installed n times (being n the number of modules) and, in addition, each module doesn’t know the instance of that external library installed in each of the other modules being impossible, in the case of react navigation, to navigate from one to another.
Another option would be to install the libraries only in the skeleton, but this also brings some problems. First of all, it becomes more difficult to develop in a local environment, since each module should be modified inside the same repository of the skeleton and then, at the time of uploading the changes, having to separate them. On the other hand, if we do not want to have that problem in the local development, we should import in each module the library coming from the skeleton, which would be impossible, because this part of the app would not be uploaded as a package, so we would not have access to it.
So, to solve this problem, is where we came up with the idea of this third part. This “engine” would basically be just another package like the others with the only difference that it will not provide any specific business area, but it will provide all external libraries and common things that are shared between the different modules throughout the app (eg: components, generic error screens, constants, images, etc). The advantages of this are several:
We already have the logic of how to do it, now let’s see how we can develop it.
The most important file is package.json. In it the first thing we must do is to add to the peerDependencies the react and react-native keys to specify that this module we are creating must use the same version of react and react native as our skeleton, which is going to import it.
Once the properties of react-native-builder-bob have been configured, we must know that this cli will compile and build our library, preparing the code ready to be consumed in a folder called “lib”, from where we must make the imports into the application.
The following properties are important to take into account
In our code we have differents possibilities to import the elements:
When working in a CI/CD controlled environment, we must be aware that the generation of executables is generally done in containers. Therefore, the imports must obviously point to the packages published in the package managers so that these containers can download their code when compiling and building executables.
So the question is, if we are working locally, how do we do to see live changes in our packages (hot reload) if later in the app will have the imports pointing to the package already published? There are 2 main solutions (or at least that’s how we found them).
The first would be to point the imports to the local file where the element I want to import is and then, after uploading the changes, modify the imports to point to the published package. Something completely tedious and also very risky.
This extension is in charge of telling babel that when compiling it must pay attention to the path of the imports and modify them as indicated in the babel.config.js file.
Example: we can tell
'example-components' to replace it with the path
'../../example/src/components' at compile time, then in our code we could have the following:
Before using the plugin:
import example from '../../example/src/components/example1.js'
After using the plugin:
import example from 'example-components/example1.js'
And babel will take care of making the effective change so that it works correctly afterwards.
So, going back to our microfrontends, what we are going to do is to import our packages pointing to the one already published in the package manager but then we are going to tell babel to modify the package name to the absolute path. This way, the only thing we have to do before publishing the app is to skip this configuration so that the containers will then take the published packages.
So, the first thing we must do, is to take in the modules inside the package.json the version of react and react-native to the peerDependencies property to, then, tell the bundler in its configuration that it must take only one instance of react in the whole app.
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.
For this, the main part of the app, that is the skeleton, must be the one who creates the global state of the application, importing the states of each module and then creating the store. It is similar to how it handles navigation:
But here we must take care about something, and it is that both skeleton and modules must always use the same instance of redux, and for this, since the external library of redux will be installed only in the engine, we can export the entire package so that it can be accessed by everyone.
In the engine we will have the following:
And then, in the base repository or skeleton, we will have the following:
Once this architecture is applied in an app, several positive things can be seen:
Finally, I would like to add that this is not the only way to do it, there may be (and in fact there will be) other similar or different ways, but of course it will always depend on the objectives of the enterprise or company for which this architecture must be applied.
In my personal opinion, this is something that all great apps should look for, no matter how difficult it results, because in the long run will weigh more on the side of benefits of the balance than on the side of the inconveniences during the process.
Eu acredito fielmente que o livro te escolhe. Nós temos pouca, ou quase nenhuma ouso dizer, decisão sobre o que vamos ler em seguida. Os melhores livros e histórias que li na vida vieram assim, do…
Welcome to the latest edition of our Weekly Update series. This week, we published the results of our blockchain audit by Deloitte, launched a new DAA, hosted our first guest AMA on Reddit, and more…
Over the past few years, the number of instances in which Internet access was blocked for entire regions by the Indian Government has been steadily climbing. These “Internet shutdowns” are usually…