Implementing a Micro-Frontend Architecture With React

At FloQast, one of our main goals is to enable high-performance teams. This article will describe one method we are using to increase our speed and efficiency by implementing a micro-frontend architecture using React.

Update: Since this article was published we have migrated to a new micro-frontend architecture. The concepts described below are still very much relevant and worth reading; we simply changed the implementation. Check out How FloQast Is Scaling Our Micro-Frontend Architecture to understand why!

Dismantling the Front-End Monolith

By now the concept of micro-services has become ubiquitous within the software industry; using loosely coupled services in lieu of a single back-end monolith. Some benefits of a service-oriented architecture include:

  • Narrowing the scope of each service, in turn reducing team overhead
  • Creating unique deployment pipelines, in turn reducing lead time (the time it takes to build, test, and deliver code)

While micro-services have helped to dismantle the back-end monolith, we are still left with a large mono-frontend. Not only does this create an overlap of responsibilities between teams, but the shared codebase means a shared deploy pipeline, which feels akin to merging a super-highway into a single lane.

By following the guiding principles of micro-services, and separating the concerns of the mono-frontend into separate clients, aka micro-frontends, we can work to enable the same benefits on the front-end.

Each client exists within its own repository and has its own unique deploy pipeline, thus enabling faster lead times and reducing the amount of overhead for each team. A central hub then composes all clients together at runtime, providing the illusion of a single application. End result: A unified user experience with a decoupled developer experience.


Source: Micro Frontends

Building Micro-Frontends with React

Each client is a complete React application built using create-react-app. All of which are composed into a single index.html file provided by the hub. The hub also manages the root-level routing to determine which client to load.

A user visits floqast.app which loads the client-hub; then, depending on the URL path, the hub will load the appropriate client.

Some modifications to the bundle produced by webpack are necessary in order to convert the clients from standalone applications to composable ones:

  1. Output a single bundle instead of chunks – This allows the hub to easily fetch the client’s bundle.
  2. Output a window module – This allows the hub to dynamically load the client’s bundle.
  3. Export, don’t render – Delegate the rendering from the client to the hub.

We use craco to override the underlying configs without ejecting. By replacing react-scripts with craco, each script first loads any config overrides defined in craco.config.js.

Here are working models of two clients: a dashboard client and an admin settings client.

The hub is also a React application, and using react-router, creates a new route for each client. The MicroClient component is responsible for dynamically loading the specified client’s bundle into the DOM using the react-loadable library.

And here is a working model of the hub. It dynamically loads the two clients shown above.

Potential Obstacles and How We Are Navigating Them

While the decoupled nature of a micro-frontend architecture helps to improve the developer experience, we don’t want to lose sight of our end users. The look and behavior of the app should remain consistent regardless of which area of the app the user is visiting. It doesn’t matter how fast we can ship code if the end result is not a pleasure to use.

Cross-app Communication

In order to provide a seamless experience across clients, they should be able to share a certain level of UI state. However, they should remain as loosely coupled from one another as possible, lest we begin to circle back into a monolith and lose the advantages we set out to gain. Therefore any communication between them should be handled indirectly and asynchronously. Some examples include:

  • Persisting to shared storage – One client writes to storage enabling others to then read from it. This may be web storage or backend storage depending on the life of the message.
  • Emitting custom events – One client emits an event enabling others to listen and respond accordingly.
An example scenario using custom events

A user visits the admin settings (settings-client), changes the active company, then navigates back to the dashboard (dashboard-client) expecting that the active company persists between page transitions.

In this scenario, when the active company is changed, the settings-client emits an event called “activeCompanyChange” along with the id of the newly selected company:

window.dispatchEvent(
  new CustomEvent('activeCompanyChange', {
    detail: { companyId: nextCompanyId },
  })
)

The dashboard-client then listens for this event and takes action:

window.addEventListener(
  'activeCompanyChange',
  evt => setActiveCompany(evt.detail.companyId)
)
// Be sure to remove any added event listeners to prevent memory leaks.

Shared Styles

The hub not only provides the top level routing, but also the common layout. This ensures each client shares the same surrounding interface. Any modifications to the layout are immediately reflected in each without any specific action required.

We also maintain a UI library from which clients may import common components. To read about the challenges we are facing in regard to managing all the CSS that style our components, please refer to Styled Components in React: Moving away from SCSS.

Shared Utilities

We’ve also encountered the concern of duplicating common utility functions across the clients. To some extent, we’ve deemed this as an acceptable approach, mainly as an effort to iterate quickly but also to ensure we avoid implementing the wrong abstraction. However, similar to our UI library, we are working to create a library of common utilities. Flodash, anyone?

Team Fragmentation

Aside from the technical challenges, there are also business concerns as well. One of which is potentially creating fragmentation in how teams work. We are combatting this by encouraging open communication amongst the relevant teams. Along with creating specific Slack channels where we can keep the conversations focused, one particular effective strategy we’ve employed is to gather representatives from each team and share a collective brain dump of the current state of affairs. We encourage everyone to throw out every question, problem, and concern. Then we work to codify the information into a digestible list. Next, we create a specific action for each item and delegate one to each team. Finally, we reconvene after some time to discuss the results. Repeat as necessary.

Where Do We Go From Here?

So far, implementing a micro-frontend architecture with React has been a success here at FloQast. This is not an endeavor unique to us, and so we will continue to explore, learn, and share while we work to continuously improve our approach.

Some additional resources to consider include:

  • Micro Frontends – A great introductory article to the concepts of micro-frontends.
  • single-spa – A framework for building micro-frontends out of the box. Their recommended setup is full of great information.
J.C. Yamokoski

J.C. is a Staff Software Engineer at FloQast who loves breaking down large complex problems into small manageable pieces and attempting to re-explain them in a simpler way. When not hacking at code, he enjoys spending time with his family.



Back to Blog