avatarStefan Haas

Summary

The article discusses implementing micro frontends using Nx, React, and Module Federation, emphasizing the benefits and challenges of this approach for large organizations with multiple development teams.

Abstract

The article delves into the concept of micro frontends, illustrating their use by major companies like IKEA and Spotify. It distinguishes micro frontends from modular frontend architectures, noting that the former is more about independent deployment and organizational structure than a technical solution. The author argues that while micro frontends can allow for technology mixing, this is generally not advisable due to increased JavaScript load and lack of knowledge transfer between teams. The key advantage of micro frontends is the ability to lazy load features at runtime, which is demonstrated through an example using Nx, a tool for enterprise applications, to create a micro frontend architecture with a host application and remote applications. The article provides a step-by-step guide on setting up this architecture, including code snippets and configuration details, and concludes by discussing the complexities and considerations involved in adopting a micro frontend approach.

Opinions

  • Micro frontends are often mistakenly compared to microservices, leading to overlooking their core purpose of independent deployment and organizational benefits.
  • The author advises against using micro frontends for projects with fewer than 10 developers, suggesting modular frontend architectures as a more suitable alternative.
  • Mixing technologies (e.g., React, Angular, Vue) in micro frontends is generally discouraged due to negative impacts on user experience and team collaboration.
  • The author expresses a preference for Nx due to its plugin system, module boundary rules, and support for micro frontend architectures within a monorepo setup.
  • The use of Module Federation is highlighted as a technically sound solution for dynamic imports of components not part of the host application, with the caveat that static references to remotes should be avoided in favor of dynamic module federation for increased flexibility.
  • The article suggests that while micro frontends can reduce time-to-market in large projects, they introduce new complexities such as handling routing, avoiding styling collisions, and ensuring a common design system across teams.

Your first React Micro Frontend

Using Nx, React and Module Federation

What do companies like IKEA, Upwork, Zalando, Spotify, Starbucks and HelloFresh have in common? The all use micro frontends.

So let’s directly dive into the topic and discuss when they make sense and how you can create a micro frontend architecture using Nx, React and Module Federation.

Micro Frontends

It feels like everyone is talking about micro frontends and automatically associates it with the perfect scalable frontend architecture. That might be because it is compared too much with micro services on the backend, which serve different purposes, hence most people overlook the obvious theoretical details of micro frontend architectures.

First of all, the core purpose of micro frontends is independent deployment. Meaning that you would like to develop parts of a frontend and ship them when they are done without depending on other parts of a frontend. Therefore you should not think of micro frontends as a technically profound architecture, and instead associate an organizational benefit for large organizations with multiple teams. So in case you are considering to implement micro frontends for a project with less than 10 developers — let me stop you right here and advice you to look for modular frontend architectures. You can achieve modularity in a monolithic frontend by leveraging a monorepository and splitting the application and features in tiny libraries which act as strong module boundaries.

To be honest, there is one other reason, where micro frontends shine, but this should not be abused. Micro Frontends allow you to mix technologies, such as React, Angular, Vue, etc.. So you could actually have a frontend that uses multiple frameworks and that is because the micro frontends can be really independent when they are wrapped in an iFrame or a web component, such that the inner workings of a micro frontend cannot affect the outer DOM and its micro frontends. But let’s take a step back and elaborate if that is even a good thing or not.

Adding various kinds of frameworks and technologies is a bad idea in 99% of cases, because your users will have a decreases UX because you have to load an enormous amount of JavaScript in the browser. On top of that it would be awful to have a React team, an Angular team and a Vue team in one organization. Knowledge would not be transferrable that easy and teams would reinvent the wheel.

But, it can be quite handy when you are doing incremental framework migrations or your company acquires a company that uses a different framework and you want to include the acquired code quickly.

That being said, the key idea of micro frontends is to lazy load features at runtime instead of compile time.

const Component = React.lazy(() => import('./OtherComponent'));
const Component = React.lazy(() => import('https://microfrontend.app/comp'));

The first example works perfectly fine, but this is just regular lazy loading done at compile time, which means that the compiler is aware of the code which the dynamic import is referencing such that it can be bundled in a lazy chunk file.

The second example does not work out of the box, because the url could return anything and the compiler does not know how to bundle such thing. But this would exactly be thing that we are trying to achieve with micro frontends.

So let’s find a technically working solution to achieve such a dynamic import of a component that is not part of the host application.

Example

In this example we will implement a minimalistic micro frontend architecture with a host application and a remote application. Both applications will be created in an Nx monorepository and use Module Federation. Personally, I really enjoy Nx because it has an extendable plugin system and has great module boundary rules which can be leveraged for architecting enterprise applications. Hence, I am always using Nx for real world projects. And monorepositories generally are a good fit for micro frontends and generally enterprise applications because they can assure a single version policy, meaning that all apps and libs in a monorepo share the same dependencies with the same versions, such that version mismatches are not possible.

Luckily, creating a micro frontend architecture with Nx is really straight-forward because they have built-in generators which will help you get started in no time.

First of all, let’s create an empty Nx workspace and make sure to check the “Integrated Monorepo” option, which should be the default for micro frontend setups, especially when you also want to share libraries in the monorepository.

Next, let’s install the @nx/react plugin inside the workspace to be able to create an Nxified React app.

And now, let the magic happen with just one single command. Nx has a generator system, such that you always write nx g <plugin>:<generator> …props. And in this case we will use the @nx/react plugin and the host generator to create an application shell, which is the React app that will lazy load the other micro frontends at runtime. We can even go a step further and generate the micro frontends with the property remotes and pass the remote application names.

You should see all of the apps generated in your workspace now.

Having a look at the generated files in the shell application we can see that Nx has created some additional configuration files to support module federation based on Webpack.

Note, that you could also use Module Federation with Vite, or Native Federation to be bundler agnostic. But Nx does not support such solution out-of-the-box as of today.

The module-federation.config.ts defines all the remotes and tells Webpack that imports with "remote1/..." or "remote2/..." shall not be checked at compile time and will be loaded at runtime.

import { ModuleFederationConfig } from '@nx/webpack';

const config: ModuleFederationConfig = {
  name: 'shell',
  remotes: ['remote1', 'remote2'],
};

export default config;

The remotes define a slightly different config, where the exposed module is defined which always points to a remote-entry file. Whereas the remote-entry file exports the App component of the remote application.

import { ModuleFederationConfig } from '@nx/webpack';

const config: ModuleFederationConfig = {
  name: 'remote1',

  exposes: {
    './Module': './src/remote-entry.ts',
  },
};

export default config;
export { default } from './app/app';

You can serve the remote applications by running npx nx serve remote1.

And now with Webpack doing some magic to allow runtime imports, we can make the dynamic runtime import, shown initially in this blog post, work. Inside the App component of the shell we can import remote1/Module and remote2/Module without compile errors.

import * as React from 'react';

import NxWelcome from './nx-welcome';

import { Link, Route, Routes } from 'react-router-dom';

const Remote1 = React.lazy(() => import('remote1/Module'));

const Remote2 = React.lazy(() => import('remote2/Module'));

export function App() {
  return (
    <React.Suspense fallback={null}>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>

        <li>
          <Link to="/remote1">Remote1</Link>
        </li>

        <li>
          <Link to="/remote2">Remote2</Link>
        </li>
      </ul>
      <Routes>
        <Route path="/" element={<NxWelcome title="shell" />} />

        <Route path="/remote1" element={<Remote1 />} />

        <Route path="/remote2" element={<Remote2 />} />
      </Routes>
    </React.Suspense>
  );
}

export default App;

Running the shell with the remotes is nothing but easy with npx nx serve shell --devRemotes=remote1,remote2

Opening localhost:4202/remote2/ will show that the remote2 app is running.

Opening the shell now on localhost:4200 will show that we can load all the remote app which are running on a different port into the shell app at runtime.

To me, this is true magic.

Find the full code here in this GitHub repo:

Conclusion

Micro frontends are very popular and many developers and architects are talking about them. The most appealing reason for micro frontends, is the possibility to have independent deployments, such that an organization can work on a large frontend with less inter-dependencies between teams. In such large projects with multiple teams, micro frontends can cut the speed-to-market, but that comes with various costs. A new complexity is added to a frontend and questions arise, such as:

  • Should we use a monorepository to have a single version policy?
  • Should we use a poly repository to be even more flexible?
  • How can micro frontends be discovered in a poly repository approach?
  • How can assets and static files, such as images, static translations be loaded without CORS errors?
  • How can we make sure there are no styling collisions?
  • How can we make sure every micro frontend has a common design system and UX?
  • How can we make sure micro frontends do not depend too much on each other?
  • How can we handle routing and nested micro frontends?

So before you start implementing a micro frontend architecture, you should answer all of these questions and find a good narrative for your organization such that the architecture fits your organizational needs the best way possible.

Note, that I only described the Module Federation approach for micro frontends in this article, but in reality you could achieve such architecture with other technologies as well. You could also use Hyperlinks, iFrames, SAP Luigi, Web Components, … but my personal favorite for sure is Module Federation.

Another side note: I should mention that this example is really trivial and in a real world app you should not statically reference remotes in the module-federation-config and instead use dynamic module federation to be more flexible. Here is an example with Angular:

React
Web Development
Programming
Software Development
JavaScript
Recommended from ReadMedium