How Micro Frontend Reshaped our Dev Forever: Real Life Story

Rotem Zifroni, Team Leader @Frontegg, talks at iJS New York (2021) about how we used micro frontend strategies to overcome front end challenges, and about the impact it had over our frontend development, architecture, CI/CD, and overall processes.


Transcription

Rotem

Hello, everyone. I’m excited to be here. Let me just share my screen. Okay, so my name is Rotem, and I’m going to speak today how micro-frontend reshaped our dev forever based on our real story.

Let me first start by introducing myself. So I live in Tel Aviv in Israel. Currently, I’m working as a front end team leader at Frontegg. And previously, I worked at Wix. I also served a few years as an officer on unit 8200 of the Israeli intelligence.

Let me tell you a little more about me, some fun stuff. So I’m also a tennis player, and I’m a professional instructor, and I’m ski obsessed as you all can see here in the pictures. I’m also baking an awesome hallah in my free time.

And I’m also a social entrepreneur. This is about me. Let’s jump in. So I first want to introduce you to Frontegg, my company that I’m working on. So Frontegg is basically helping SaaS companies be focused on the core product. How do we do it?

We providing them with all the necessary feature of the user management layer. How we actually do this, we develop two main component. We develop a login box, an advanced one with multi-tenancy and advanced feature built in. And we also develop our admin portal, which is integrated within the application and allowing the end user to do whatever they want in any aspect of their account.

As you can see here in the image and I hope it demonstrating it well, so our components basically sits on top of the product of our users. We are doing it without interfere the product or affecting their product, and they can be focused on implementing their competitive edge.

We also let them customize our component to match their brand style. Now that we’re all aligned what we are actually doing, let me tell you about our challenges while developing our product in the front end side.

I mean, this is why we’re here. So first of all, our component is embedded in our customer’s application and it has to be while actually they are not an integral part of the product. And therefore, our components must be cross-platform. Our customers might build their application on top of every possible framework and we have to deal with it, and also we need to find a way to share the state in and out.

We hold our state in the store and we want to share it with the customer’s application and also we need some runtime application from the customer’s app. So how are we going to deal with all that? Yes. So as you can see, we had a long, long way to go, and we’re going to talk about it and see how we figured it out.

In the following few slide, I’m going to show you why micro-frontend approach helped us to face most of those challenges. So first, let’s talk about micro-frontend. The idea of micro-frontend architecture is basically taking the frontend application and decompose it into individual micro-ops that are composed together to one application, a cohesive one, that feels unified to the user.

This is kind of like taking the microservices approach from the backend to the frontend world. So as you can see here in the picture, here in the demonstration, we have a few micro-frontend. Each one of them is fully independent in terms of building and testing and the whole pipeline, all the way to production, and at the end they are taking place in the main application.

With this approach, we are talking about independent teams which can build independent features and take care of them end-to-end from the code base to the deployment, the pipelines and web technologies and everything, which is pretty convenient I think. I think we can all agree that breaking things is bliss.

So we get a few wins from this approach. I will name a few of them. We are getting flexible technology choices because right now each team can make his own technology choices. For example, we can have one team developing in Vue.js and other team developing with React. Also, we get scalable development since we can easily spin up new teams and deliver new frontend functionality.

Easier maintenance, thanks to very small repository that are very easily to understood, and also faster development since each team right now has its own CI/CD pipeline. Also, upgrading and rewrite parts of the code are getting much, much more easy, thanks to small application.

The testing is easier when we are testing small parts. And most of all, we have now autonomous team which each one of them are expert in its own domain, which is super cool. It sounds awesome, I know, and it also sounds very easy, but as soon as you’re going to implement micro-frontend, you’re going to realize that there is a few challenges in order to make it an actually good one.

So first of all, we need now to manage a few separate infrastructure, and also we need to implement design in two places while we have only one application and we want to keep a user experience consistency. And we might find it a bit challenging and we might need to implement some shared component library or CSS libraries.

Also, most important, we don’t want our user to accidentally realize that when he navigated to another page, we render the new application. We want to create a seamlessly experience to the user. Then we’re talking about CSS, like how the hierarchy will work. One micro-frontend will override the other.

And what if you want them both to be independent? And most of all, we’re talking about shared libraries and the shared state until now when we had to manage state for one application. And right now we probably need a few application to share the same content. So how do we going to do all that?

It’s time to get our hands dirty and get to business and talking about the most popular way to do micro-frontend. So we have three options, iframe, web components, and dynamic module resolving. Let’s dive into each one of them.

So iframe. We used to call it the naive approach as most of you, and I guess most of you are familiar. So just a reminder, iframe are an HTML documents that can be embedded inside another HTML document. I’m going to show you right now how it would have looked like if we implement our micro-frontend, the login box, and the admin portal by using iframe approach.

So here in the code sample, we have two containers at the top. One of them is for the login box and the other one is for the admin portal. We also have a render function which is basically creating an iframe tag and then set the source of the iframe to be our bundle of the application based on the module name.

We then use this render function twice, one, to render the log inbox inside his container, and then to render the admin portal inside his container. And then we are getting now in the DOM 2 iframe representing our micro-frontends. Let’s talk about why it could work.

So basically iframe, by their nature, make it very easy to build a page out of independent subpages. As you can see, this is very easy. And also iframes are offering us a very good isolation when we’re talking about styling and global variable because they are actually a full browser page that is not related to other browser pages.

When we’re talking about the communication, so the communication of iframe is very unrestricted, and this is based on the window events. We can dispatch an event and also subscribe to an event without knowing who is going to listen. But what is the catch here?

So first of all, the integration between a few parts of the application when we’re using iframe are much, much more restricted. For example, when we talk about routing, so iframe and domain application cannot share the same router. And also, you might find some breaking of the navigation when you go inside the iframe and go back and get stuck inside the iframe because each one of them have its own JavaScript.

And also iframe can access the parent DOM but it cannot access the JavaScript of the parent because each of them have separated event loop. Most of all, and the main catch is that the communication is not secure when we want to communicate between iframes.

So we are using the window, which means anyone who have access to the window might see our information, which is not going to work if we have private information. Also, as I mentioned, iframe cannot share libraries with the main application, which will be problematic if you want to talk about some shared context. So let’s go to the other approach which we call the standard approach because I think it might be the most common one, the web components.

Actually, web components are a runtime integration implementing by simply defining an HTML custom element for the container then to be instantiate. Let’s see right now how rendering our micro-frontend with web component would have looked like using custom elements.

As you can see here, we have two tags at the top, which we’re going to talk about them, but I want you to look at the bottom of the sample where we are using the API of define. We are defining two custom element with web components and API. The defined method is getting two arguments.

The first argument will be the custom element tag name which we want to use. In our case, it will be the login box and the admin portal. And the second argument will be a class that is extending HTML element and must implement a function called connected callback.

Connected callback will be the method responsible for mounting and rendering the component. In our case, as you can see, we are using our application mount function in order to render, and this function will be invoked each time the custom element is appended to the DOM, which in our case will happen once because we are using the custom element once at the top. The communication here is established with two ways.

I demonstrated here for you the event way, which is very similar to iframe. We are using the window to communicate. But web component is also offering other way to communicate with attributes. Why this approach can work. So most important, web component can work with any JavaScript library or any framework.

The interacting here is very simple by using events or attributes. The components are independent. With every other component, the browser is involved with the render and the lifecycle of each element.

But here we are talking about a fully independent component that is managing themself, they’re managing their render and removing from the DOM by themself. The browser is not aware, which is pretty cool. And in this way, we are getting teams that each team can work on his own web technology of choices. Also, the web component are offering us with a multiple concepts.

We talked about the custom element but they’re also offering us a shadow DOM, which is a fully private DOM that help us isolate CSS and JavaScript, and the HTML template which help you create some template for your component. But what is the catch with this approach? So usually, the catch here is the security because here, as I said, the communication is also based on the window, and everyone we have access to might get our private information.

This leading us to the third option which we like to call the not-for-beginners approach, the dynamic module resolving. The idea here is actually that the main dashboard is built with placeholder for each of the micro-frontend but without actually including them in the build.

The parent application will be then responsible to retrieve those child application and those micro-frontend dynamically from the CDN. Let’s see how it would have looked like with our micro-frontend. So again here, same concept. We have a render method, which in this case is first fetching from the CDN our bundle.

It is doing it by using our base URL with the module name. Then we have the JavaScript of our application, and we have one more step to do. We are modifying it by adding it the global store instance to the JavaScript. So each of the micro-frontend will have the same context. We then just append the JavaScript to the DOM, as you can see.

And now we are just getting two of our micro-frontend running inside the DOM. Let’s elaborate more on this approach. So we get here also a domain-driven team working on domain-driven micro-frontend fully end-to-end as we explained before.

Now each of the micro-frontend will be now a standalone chunk. What it actually means is that the chunk is including all the dependency he needs inside the chunk beside the shared libraries. Our shared library is extracted to then be resolved in the micro-frontend from global object as you just saw with the store.

So in this way, the main repository isn’t been affecting with every change of each component, which is very good. And also since we are using REST API to get our CDN bundles, rolling back here will be much more simple because simply we can set a version with the REST API and roll back the latest version. Worth to mention that in this approach, also the main bundle size will be less than in other cases because we are first rendering the main application, and after the page is fully loaded, we are lazy loading each of the micro-frontend.

So it sound good, but we have a few catch here. First of all, obviously, this is much more simple to use static bundles. When we’re talking about dynamic bundles, we need to build a whole infrastructure for loading those dynamic bundle based on the deployment registry. For example, how do we choose the right version?

How do we know if we now need the stable version or the latest one? And when we download from CDN, we need to make sure that we have all the dependency, that we have the CSS, that we’re keeping version aligning between the micro-frontend. We are also talking here about very complicated pipelines. I’m talking here mainly about our GitHub action, which might be very complex with dealing with the version.

How do we release an alpha version to test it? Also, the CDN approach cannot work with content security policy. As you probably know, many of the application are using CSP in order to add a layer of security that is helping them detect some types of attacks. With this approach, downloading the script from the CDN and then modifies them might be problematic. Also, this approach cannot work with Chrome extension.

This is the time to talk about our story. Up until now, we talk about the theory, and right now I want to take you to our story in Frontegg and show you what we actually done. Let’s start from the base point.

So our base point. So as a background to the base point, as I mentioned before, we have a lot of customers that is building their application on top of every framework, ranging from Vue.js to Next.js and even Nuxt. And therefore, our components must be supported with each of this framework.

We probably don’t want to lose customers for this. So we’ve started by implementing our library separately for each framework. Yeah, yeah, this is exactly what you hear. We implemented the login box and the admin portal separately for each framework. Why it cannot work.

So I’m sure most of you can guess. So we had to develop every feature multiple times. Think about it, we had to do tech design each time and the POC separately for each framework. Also, we had to test separately each of the library, which is also complicated. Then we also need to maintain expertise in each of the framework.

Our application is not so simple. And the delivery was bad. You can imagine that we have one feature that we implemented in one framework, but we didn’t implement it yet in the other framework. So how are we going to communicate this to the customer? And most of all, this is a never-ending framework story.

Tomorrow we can have a new customer that is using Flutter Web or the other way is going to…or the other customer is going to use Next.js and catching up here are getting very, very complicated. You know, we first try.

We focused about choosing only one framework, as you could guess. In our case, we wanted to implement everything with React. And now since our customer is using this many framework, we need to find a way to render our application inside all of those framework. In order to do it, we chose to use the dynamic module resolving approach.

As I mentioned before, the concept is that now our main application is holding just two placeholder, one of them for the login box and the other one for the admin portal, but we are not including them in the build. The main build, the main bundle which inside the build is then responsible to first initiate a DOM containers dynamically for each of our micro-frontend and then is responsible to render dynamically imported bundles from the CDN.

A little bit more in detail. So our micro-frontend are implemented with React. And then when we publish a new version, we actually upload the bundle to the CDN. Each of our micro-frontend are in standalone chunk, and the shared dependency are keeping outside. Each of our…the shared library are now in the main bundle and the micro-frontend is going to be resolved by the main bundle from global object.

Now we get micro-frontend that are fully independent in the customer’s application. Imagine they have everything they need. The dependency are inside the bundle and the shared one are being initiated from the main bundle. And right now we don’t need anything from the customer’s application, which means we can be rendering in any possible framework.

I hope this structure is demonstrating a bit well the approach. So let’s start from the left. So we implementing our application and then we’re using GitHub action in order to have very complicated pipelines to publish the new version to the CDN.

And at the bottom, you can see, at the other edge, the customer’s application, the main dashboard, which is using our Frontegg package, and the Frontegg package is then downloading the bundle from the CDM. It’s still not good enough as you can guess. So mainly our customers is using content security policy.

And we can’t let them lose us. So we couldn’t use this approach. And as I mentioned before, our pipelines and managing the versioning and the hierarchy in the CDN is getting very, very complex. One more thing is that we run into situation where we are changing the version for the customer without the customer actually did an active thing for it.

So it might have a confusing experience for our customers. In our second try, we wanted to keep exactly the same approach, but right now avoiding the CDN. So our main bundle is now loading the micro-frontend from lazy loading as a chunk of JavaScript instead of CDN.

We are now extracting all the dependency to be on the main bundle. Why we actually doing it? Because we want the main bundle to hold the share context, to hold the store. And then we want each of the micro-frontend to have the same context. Also, we want to enable tree shaking between our micro-frontend and also with the customer’s application.

We want to avoid duplication and also to keep alignment of version, which is also a big win here. Each of the micro-frontend is now including the code itself and nothing more. And our NPM package is right now including the main bundle and also all of the micro-frontend. So what we had so far, a quick reminder.

So we started with a bed base point where we have a different library for each framework. Then we wanted to be focused on just one framework using React and find a way to render it in all of the framework using CDN.

At the second try, we wanted to avoid the CDN for a multiple reason and now we’re using ESM modules. Let’s move on. This is still not enough because right now we need some runtime information from our customer’s application. As you can see here in the images, we want, for example, the customer to be able to customize the login box and passing us and providing us with some customization options.

Also, we want to know what host we need to run on. And sometime we even need them to share with us the routing object so we can integrate on top of it. The mission here, in conclusion, is that we need to find a way to communicate with the customer’s application and share with him some information on runtime.

The third try is talking about how we chose to do it with injecting props. So there is a few ways to communicate, as I mentioned before. We could use event or even address bar param, but both of them are, first, not secure, and second, they’re not working so good with big interface.

And I’m talking here about the big interface which I need to get from the customer’s application. So the concept here is that the main application is initializing the micro-frontend with some properties. How we did it in our way. So our library is now exposing a provider and the customer’s application, he’s using this provider to provide us with an interface of options.

It provide us as a props. I will soon show. Our initializer, he’s then initialize the Frontegg app instance, providing him with those option, and the instance he’s now sharing those option as the shared context for the communication between the micro-frontend and the customer’s app.

Our micro-frontend are now initialized with this option and also updated with every change of them. Let’s see how it looks like, starting here from the left. So we have the customer’s application that is providing our initializer with option. The initializer is in it, the instance, with this option, and it’s stored the context.

Then the instance is starting our micro-frontend providing them with the options, and then each of the micro-frontend can subscribe to every change of the context. And how it actually look on the customer side. I’m sure this will be more clear.

So this is a React customer that on top is using our Frontegg provider from our library, and then you render the Frontegg provider with some options. As you can see here, for example, the user is providing us with a base URL which will tell us what host to run on, and then he telling us that he want his login box to be rendered with a specific background color.

We’re almost there, but this is not good enough because right now we need to share with the customer’s application our state managing our store. For example, a user is logged in via our login box and then when he is authenticated, we need to let the main application to know about it and we need to provide them the user data and the access token, and some more information.

The mission here is to actually take our store and providing the customer’s application with it, and also to communicate between our micro-frontend. I mean, the login box need also to update the admin portal with the user, the authenticated user. But we have a small catch here.

We have our state and we want to share it. The only thing is that our React customers are expecting to be provided with hooks, but our Angular are expecting observables, and Vue.js customers are expecting mixins. So how are we going to do that? The mission here is to communicate our React store with various framework. How we actually did it.

So our micro-frontend store is managed now with React Redux store, and for each of the framework, we implemented an important wrapper. This wrapper has a few responsibility. So one of them is to hold an adapter to glue our React Redux store to every relevant state management technology for each of the framework.

Then the wrapper is wrapping the customers up with a store provider. And then it is also exposing it for all the actions API from our store. With this situation, the customer’s application are now able to subscribe to our store, get updated for every change, and also dispatch action in order to implement their own application upon our store, which is amazing.

This is demonstrating the structure. So we have the customer’s application on the left using one of our libraries. Our libraries are initializing the Frontegg instance and then subscribe to the store, the same store that each of the micro-frontend is using.

Then with every change of the store, the library have the store adapter and adapting it, so now the customer’s application will be able to subscribe also to our store in the relevant state management technology. I’m going to show you now how is it look like on the customer’s application so it will be much more clear.

Let’s start with React. So here is a React customer who is using a hook. They use Auth hook from our library. And as you can see, he’s using his authenticated information in order to know if the user is authenticated or not.

And if he is authenticated, it now use the profile picture of him and the name and rendering it to the DOM. We now have an Angular user who is implementing the Angie OnIt method and using our Frontegg app Auth service from our library.

And as you can see, he subscribed for two states, that is authenticated and the user. Same as we saw before. In this way, he’s going to be updated with every change. Here is his application, and as you can see, in the same way, if the user is authenticated, he’s showing the profile picture and the username.

This is a view user. I can tell that in a view. When the user is rendering our library, we are injecting the mapper, which we call mapAuthState, to the application in a way that now every component have an access to this method.

Here in the component, you can see that in the data, we are using the mapAuthState. In this way, this component is actually subscribing to every change in this Auth state. You can see on top that here the user is using the authState.user in order to show his email if he is really authenticated.

A quick summary. We are almost there. What we had so far. Started with multiple frameworks, which was very bad, then focusing on one framework, which was React, and then using CDN. Then we understood we want to avoid CDN and started using ESM modules. Then we talked about how we communicate with the customer’s application and between our micro-frontend, and we show how we do it.

We inject some props and with having an adapter for each of the framework to expose our store. One last thing left for us to do. As I mentioned before, we want to avoid CSS clashes. Actually, we don’t want our micro-frontend to be affected from external libraries, and most important, we need to keep our customers’ application safe in this term, isolated.

Here we are adopting the web components approach. So now instead of having regular containers, regular component containers inside the DOM, we are now using the shadow DOM. A quick reminder, the shadow DOM is helping us creating a private DOM which is completely isolated.

So right now, each of our micro-frontend is not affected from external CSS. And most important, the parent application, which will be the customer’s application, is also not affected by our micro-frontend. This is how it looks like in the DOM.

Pretty amazing, to my opinion. So we have the Frontegg application, this is a custom element, and inside it, we can find two containers, the Frontegg admin portal container and the login box. And inside each one of them, we have a completely private DOM, thanks to the amazing API of web components using the shadow DOM. And I think we are ready to go.

So I’m now showing you how a user is logging in via our login box, and then the main application is updated about it and telling me, “Hello Rotem!” with the user information. When I now enter the admin portal, the admin portal is also updated that I’m logged in and showing me my profile with all the relevant information.

You can see that when I sign up right now. So the application is also updated that there isn’t an authenticated user and it’s showing the login box once again. Cool. And after we are passing through all of these challenges and we had a long, long way to go, I want to tell you about the oasis we have right now at Frontegg.

So we develop everything only in one place, which is awesome. And we have separated pipelines for each of the framework that are triggered automatically. Every time I’m publishing a new version of the admin portal and the login box, our wrapper are triggered automatically and being updated. The changes that we are actually taking of each of the wrapper are very, very minimal, and trust me, every developer of my team can achieve them.

The delivery is now awesome because we have our core application only in one place and our wrappers are almost always aligned. Dev velocity is now much, much better, thanks to all of these and also very fast deployments. And most important, now adding a new framework to our application is blazing fast, which is very, very cool.

So here our story is coming to an end. We probably will have a few more challenges, but this is how micro-frontend actually helped us make an awesome application and a very advanced one.

And that’s it. This is the time for questions.

Participant

So thank you, Rotem, for this interesting presentation. While the guys have free time to type these questions in, maybe I have a question. So Rotem, you mentioned that you don’t want to implement for each framework, so the solution you choose to share your store, isn’t it complicated to do it for each framework?

Rotem

Yeah, so thanks for this question. Actually, this is a very good one. So I will just repeat it. He’s asking about, we’re talking about not being independent in each of the framework but then we have our store adapter for each of the framework. So he is asking, like, if this is maybe complicated again.

But the answer here is that our adapter are extremely simple. They’re just taking one store and converting into another. Each of the wrapper contains only three files and it’s very, very minimal to then change it and even implement a new one.

So this is very convenient and we don’t have a plan to change it at the moment.

The question is, how’s the development process? Does the engineer required to jump between code spaces very often?

Participant

How is the development process? Does the engineer required to jump between code bases very often?

Rotem

No. So most of our developers are implementing React. We are implementing the admin portal and the login box. Most of our code is there. Only if we have a massive change or a breaking changer or something that we want to change in the API of the store then we have some dedicated developers that are very familiar with this code and can easily achieve this change.

But no, they’re not passing between code bases. They have mostly one, which they have enough to do there.

Participant

Could you maybe tell us something about how we can contact you if there are questions regarding to the topics you mentioned in your presentation?

Rotem

Yeah, sure. So I will be very happy to answer further question and to even share with you some more code samples or show you how we actually do some part. You can contact me via LinkedIn, feel free. You can also contact me with Facebook, wherever you wish.

Participant

How long did it take to get to the fourth and latest iteration of the project?

Rotem

Okay. So I think, like, there was a few steps here that took a while. You know, like, restructuring to work with one framework was the greatest milestone here. And the challenges later on were smaller. I mean, changing the structure to not work with CDN and to work with ESM modules was not too complicated.

It even simplifies most of our things. So I guess it took somewhere around two weeks. And then adding the store communication was also very easy. I mean, the challenging part was coming with this idea. And we have this greatest architect, which worked a lot in order to come with this generic solution.

But actually implementing this was very, very easy and straightforward.

If you have other questions, don’t miss to stop at the Frontegg booth here in the expo.