A micro… bunny 🐰

How to build Micro Frontends with React?

Adrien Baron
Cazoo Technology Blog
10 min readMar 31, 2021

--

EDIT: I have now implemented the approach presented below as an open-source project called tiny-frontend 🐰. If you’re interested to use this architecture in your projects after reading this article, feel free to have a try!

Front End at Cazoo

Cazoo is a fast growing startup that is transforming the way people buy used cars. We offer a fully online car buying experience for customers in the UK. Customers can find a car on our site, buy it in just a few minutes, and get it delivered to their door.

We practise Domain Driven Design. Teams are cross functional and own their business domain. We want each team to be experts in their own business domain, and drive the customer experience for it.

For example the Search and Browse team owns the home page, search page and vehicle detail page, and the Checkout team owns every page of the checkout flow.

Our front end follows a microsite architecture. Teams share a gateway and all own specific routes on the cazoo.co.uk domain. This architecture lets teams deploy to production independently and reduces dependencies.

A Micro Site Architecture

An ownership problem

While this model works very well for most cases, there are still some cases where page level splitting is not granular enough.

For example:

  • What if the Consumer Finance team wants to show a finance calculator on the vehicle detail page that is owned by Search and Browse?
  • What if the Logistics team wants to let users reschedule their delivery when they are logged in their user account?
Teams just want to provide features

For a lot of those cases, we are currently using APIs for interoperability. As an example, let’s look at the finance calculator use case:

  • Finance provides an API that can return quotes.
  • The Search and Browse codebase contains a FinanceCalculator component that calls this API and displays quotes to the user.
Finance calculator front end owned by Search And Browse

This works great, however now we have an ownership problem. Either:

  • Search and Browse owns that front end, and we are leaking Consumer Finance business concerns to that team.
  • Consumer Finance owns that front end, but has a deployment dependency on Search and Browse and cannot deploy changes independently to that component.

What do we want?

Ideally we would want the Consumer Finance team to:

  • Own both the front end and the API of that calculator
  • Deploy that component independently
  • Be able to test that component in isolation

We would want the Search and Browse team to:

  • Consume that component with minimum effort
  • Not have to redeploy when the finance calculator changes
  • Make sure their code behaves correctly in response to the contract with that component
Search and Browse knows nothing of how a Finance Calculators works!

Micro Frontends to the rescue!

For those screaming at your screen right now, you’re right, micro frontends might help here!

The main goal of micro frontends is to get code deployed separately running in a cohesive fashion on a single page. Sounds exactly like what we need!

Initial experiment: web components

The micro frontend philosophy pushes for solutions that are tech stack agnostic. This means each team should be able to choose their own stack.

Following that, we initially experimented with web components. However, we encountered a few hurdles:

  • Web Components are not supported in IE 11, and polyfills for them are quite heavy.
  • Making React work in a Web Component is tricky due to the way React handles DOM events.
  • Wrapping Styled Components in a Web Component is also complicated due to shadow DOM.

Another downside of that solution is that we couldn’t share dependencies between components. This means that clients might have to download multiple versions of React and Styled Components which are ubiquitous in our codebases.

You get React, and you get React! Everyone gets React!

React Based Micro Frontend

The good thing about Cazoo having grown so fast is that our stack is very homogenous. We use React and Styled Components for all front ends. So why not just use React as the glue between those shared components?

Let’s say the Logistics team wants to let users reschedule their delivery in My Account:

  • The Logistics Team would develop a RescheduleHandover component and publish it as a JS bundle
  • The My Account team would pull the latest JS bundle at runtime and use the RescheduleHandover component
  • The TypeScript type of the component would act as the contract between teams
My Account loading a React Component from Logistics at runtime

Well, that’s exactly what we have done!

Lazy React

First, can React load a component dynamically at runtime? Yes! React supports asynchronously loading a component using React.Lazy and Suspense.

This is usually used for code splitting:

In this example import references a local component. At build time, the bundler (webpack for example) will create a separate bundle containing OtherComponent. At runtime, when MyComponent gets loaded, the bundle containing OtherComponent will be fetched. While that happens the user will see the Suspense fallback.

So React already has the ability to have components loaded from the network at runtime! However, the glue is done at build time by webpack. Can we change that?

It turns out, React.lazy() statement is not magic ✨. All it expects is a function returning a Promise of an ES Module with the component as a default export. This means this is valid code:

Will the component load? Suspense…

Great! Now we just need to find a way to get RescheduleHandover from the network at runtime!

Sidenote: React.lazy/Suspense is not strictly needed for this to work, check out Annex 2 for some example code that does not use React.lazy 👍.

Loading a React Component from the network

This is the flow we have decided to go for:

  • The My Account app requests an API endpoint with the name and version of the bundle it wants.
  • The API looks in a table for the latest JS file deployed for that bundle version.
  • It then returns the public path to that file to the client.
  • The consumer creates a script tag to load that JS file.
  • Once the script is loaded, the consumer expects to find RescheduleHandover exposed on the global scope.
How to load your component from the network in 3 easy steps!

We decided to use webpack to bundle RescheduleHandover. This lets us:

  • Consume the local version of React and Styled Components as webpack externals.
  • Expose the RescheduleHandover on global for the consumer to use

This works great:

For the Logistics team providing RescheduleHandover:

  • ✅ They can push non-breaking changes by pushing a new JS bundle to the latest version.
  • ✅ For breaking changes, they increase the version they push to.

For the My Account team consuming RescheduleHandover:

  • ✅ Users will get the latest iteration of the component when they reload the page.
  • ✅ The team needs to opt-in to breaking changes by increasing the version they pull the JS bundle for.

However, this comes with the following problems:

  • ❌ The TypeScript type for RescheduleHandover needs to be duplicated between Logistics and My Account.
  • RescheduleHandover required React version or Styled Components version might not be compatible with those provided by My Account at runtime.
  • ❌ All the consumers of RescheduleHandover need to duplicate the work of consuming it from the network.
  • ❌ My Account needs to know where to find the component on the global scope, and which webpack externals to expose.

Making the consumer’s life easier

Looking at the method that simply returned a React component to React.lazy, it felt like a much nicer interface.
My Account doesn't really care about how getting the RescheduleHandover happens. All it wants is:

  • Opt-in to a version of RescheduleHandover.
  • Know the TypeScript type of RescheduleHandover in that version.
  • Know which dependencies it needs to have to consume that version of RescheduleHandover.

To solve this we decided to go with a contract, taking the shape of an npm package published by the team providing the component.

First, the Logistics team publishes a @cazoo-uk/reschedule-handover package that:

  • Exposes a getRescheduleHandoverAsync function
  • Exposes the TypeScript type for RescheduleHandover
  • Declares the version of React and Styled Components it expects as peer dependencies.

Then, My Account just opts in to a version of that contract, and makes sure it matches the peer dependencies version:

At runtime, the getRescheduleHandoverAsync function exposed by @cazoo-uk/reschedule-handover will always return the latest deployed component for that version.

My Account doesn’t need to know anything about how to load the component!

The great thing with this approach is that we get the best of both worlds:

  • Automatic enrolment for non-breaking changes
  • Voluntary upgrade to breaking changes

Sharing the fun

One last problem is that the Logistics team now owns how to publish and consume new versions of their component at runtime. As more and more teams want to publish micro frontends we don’t want to have that logic duplicated everywhere.

Enters @cazoo-uk/shared-libraries!

This package abstracts away how to publish and consume ES modules at runtime. It provides:

  • getWebpackConfigForSharedLibraryBundle: a script to generate output and externals in the webpack bundle configuration
  • publishLibrary: a script to publish a new bundle for a given remote library version
  • getRemoteSharedLibrary: a function to consume a remote library at runtime

Publishing the Micro Frontend: Logistics

First, we have a repository responsible for publishing the RescheduleHandover micro frontend component and owned by the Logistics team.

This repository contains two folders:

  • app: The source of the component that will be bundled and deployed
  • contract: The npm package for consumers

app

This folder contains:

  • The source and tests of the RescheduleHandover component
  • A simple react app to run the component locally in isolation
  • A webpack.config.bundle.js file used to build the JS bundle
  • A script used to deploy the bundle

Webpack configuration
This configuration is used to build the bundle containing the component.

Webpack Configuration

The dependencies array is used to generate the webpack externals. These will tell webpack where to look for React and Styled Components on the global scope when the bundle is loaded in the consumer app.

At runtime, the getRemoteSharedLibrary function, also provided by shared-libraries, will get those dependencies from the consumer and set them on the global scope.

Flow of dependencies provided by the consumer at runtime

Deploy script
This script runs on our CI to push new releases of the JS bundle:

How the JS bundle containing the component is deployed

contract

This is just a small npm package bundled using rollup and published on our private npm repository.

It exposes the RescheduleHandover TypeScript type and a getRescheduleHandoverAsync function for consumers:

You may have noticed the dependencies parameter. React and Styled Components are marked as peer dependencies of the npm package. We also tell Rollup to not bundle them by marking them as externals.

This means the consumer bundler will be providing its own versions of those dependencies at build time.
As mentioned in the webpack configuration section, they will then be exposed at runtime by getRemoteSharedLibraries before loading the remote JS bundle.
For more details, you can have a look at the code in the Annex at the bottom of this article.

Consuming the Micro Frontend: My Account

How does this look from the consumer’s point of view?

My Account application depends on the @cazoo-uk/reschedule-handover package. Then in a reschedule-component.tsx file it simply calls getRescheduleHandoverAsync:

RescheduleHandoverComponent can then be used like any regular React component.

That’s it 🎉!

Conclusion

Let’s go back to what we wanted:

For the team providing the component we wanted:

  • ✅ Own both the front end and the API
  • ✅ Deploy that component independently
    - Possible as long as there are no breaking changes to the contract (TypeScript type or peer dependencies)
  • ✅ Be able to test the feature in isolation

For the team consuming the component we wanted:

  • ✅ Consume the component with minimum effort
    - Add an npm dependency, call a function and wrap the result in Suspense
  • ✅ Not have to redeploy when the consumed component changes
    - They do not have to redeploy for non-breaking changes.
    - They need to opt-in to breaking changes in the TypeScript type and peer dependencies.
  • ✅ Make sure their code behaves correctly in response to the contract with that component.
    - They can write unit tests against a mock following the TypeScript type provided by the npm package they consume.

✅ We also managed to do this while sharing some dependencies between the provider and the consumer!

We are currently only using this approach for the scheduling component, but we are looking forward to experimenting on other use cases.
Thank you for reading this article, we’re curious to hear your thoughts in the comments!

If you enjoy these kinds of challenges, and you’re interested in joining us, Cazoo is hiring!

EDIT: If you want to follow this approach on your projects, I made an open-source implementation of this architecture called tiny-frontend 🐰, along with documentation on how to deploy everything you need.

Annex 1: Shared Libraries code

So what do those magic functions from Shared Libraries do? Well not that much. I’ve omitted error handling for simplicity:

getWebpackConfigForSharedLibraryBundle.ts
getRemoteSharedLibrary.ts

Annex 2: Loading a remote component without React.lazy

If you’re using Next.js then you’ll quickly notice that Suspense is not supported on SSR.
Instead of having code to handle not running Suspense on SSR you can simply load your remote component in a useEffect block:

Loading a remote component without using React.lazy and Suspense

--

--