An eCommerce site is extremely data rich; from search to store, to cart, to pricing, there are many facets that need to be managed effectively. Moreover, in an age of personalization and with users expecting fast and frictionless experiences, it’s important to ensure that this sophisticated web of moving parts and different sets of data can all work together seamlessly.Â
Â
It’s possible to achieve this with a monolithic architecture, but it does pose some challenges. Because the various aspects that make up your eCommerce site — like, for example, your cart and catalog — all sit inside the same app, when it comes to scaling or amending your application it can take considerable development time (and money). This means that while it might let you deliver a decent basic experience for your customers, achieving the level of quality you want — and your customers likely expect — can be very difficult.Â
Â
This is one reason why the engineering teams around the world are moving away from monolithic architecture and towards micro frontends. Micro frontends are, not unlike microservices, modular — they are isolated frontends for particular domains (like your catalog or cart) which can be built and deployed independently than the rest of the application and makes customization and optimization easier than they would be in a monolithic context.Â
Â
But micro frontends aren’t without challenges. While modularity can be an advantage, it also means you need to do a little more work to get the different parts — in this case different micro frontends — to talk to one another.Â
Â
There’s not a single solution for doing this. In this blog post we’ll look at a number of different ways to get micro frontends to talk to one another, and explore which is most effective for different scenarios and contexts.Â
Please note that the code snippets added in the article are for example purpose only and might need some tweaks before running.
Using props
Â
Passing properties as attributes — known as props — in component-based architecture is the most basic technique for cross micro frontend communication, where the container app is maintaining the state and passing it to the required micro frontends.
Â
Consider the example of an eCommerce site with a micro frontends for its catalog and its cart. Here the catalog micro frontend would take a callback to add a product to the cart. This is responsible for increasing the total count and passing the count further to the cart micro frontend.
import {Catalog} from "@mfe/catalog";
import {Cart} from "@mfe/cart";
Â
const App = () => {
 const [productsInCartCount, setProductsInCartCount] = useState(0);
Â
 const addToCart = () => {
  setProductsInCartCount(productsInCartCount + 1);
 }
Â
 return (
  <>
   <Catalog onAddToCart={addToCart} />
   <Cart productsCount={productsInCartCount} />
  </>
 )
}
The technique is effective in case of build time micro frontend integration pattern as it is a component-based architecture, which passes the data.Â
It’s one of the best known techniques for passing data in component-based architecture.
Â
Most frameworks support this method of using props.
Â
You can always use framework constructs to avoid prop-drilling issues e.g., React Context, etc.
It adds coupling between the micro frontends and the container app.
Â
The two micro frontends must use the same framework.
Â
It can impact the overall performance of the application as multiple unwanted layers will be re-rendered with every state change.
Using platform storage APIs
Â
In this technique, we can leverage the platform's built-in storage APIs like Local Storage in browsers and Async Storage in cross-platform solutions like react Native for mobile micro frontends too.
Â
We can create a simple storage utility library exposing setter and getter from storage APIs. Now, instead of making micro frontends communicate via the container app, each micro frontend can set as well as read the data directly.Â
Â
Returning to the example of the catalog and cart, both the micro frontends are consuming the storage utility library. The catalog micro frontend is setting the latest count of added products in the storage, which is then read by the cart micro frontend.
// storage util
const storage = {
 getItem: (item) => {
  JSON.parse(localStorage.getItem(item));
 }
Â
 setItem: (item, value) => {
  JSON.stringify(localStorage.setItem(item, value));
 }
}
Â
// Catalog MFE
import {storage} from "@mfe-utils";
Â
const Catalog = () => {
 const addToCart = () => {  Â
  const productsInCartCount =  storage.getItem('productsInCartCount');
  storage.setItem('productsInCartCount', productsInCartCount + 1);
 }
 return (
  <Product onAddToCart={addToCart} />
 )
}
Â
// Cart MFE
import {storage} from "@mfe-utils";
Â
const Cart = () => {
 const productsInCartCount = storage.getItem('productsInCartCount');
 Â
 return (
  <NumberOfProductsAdded count={productsInCartCount} />
 )
};
This technique can work for build as well as runtime integration of micro frontends. Since the storage APIs provided by different platforms do not follow the subscription pattern, the micro frontend will not immediately read the new data when it is updated by the other micro frontend. So, an asynchronous and pub sub model needs to be built. The technique will be more useful when two micro frontends are rendered in two different screens as the reader can fetch the updated data on the mount itself.
It’s available for browsers as well as mobile devices. You’ll need to use local storage for browsers and Async storage for mobile apps.
Â
It involves looser coupling compared to passing props between the app and micro frontend but hard to debug which micro frontend is setting the data.
It’s not a scalable solution for bigger applications. However, it can be used for a small set of data. To avoid ambiguity, the data set must be namespaced into platform storage according to the app name.
Â
It’s also not a secure technique for saving protected data.
Using custom events
Â
This technique is suitable for web micro frontends and is scalable for runtime micro frontends. The main idea here is to use the browser’s in-built custom event APIs to publish events with the data from one micro frontend. The other micro frontends subscribe to the events to get the data. This approach technique is very similar to events-driven architecture in the microservices world.
Â
In the catalog and cart example, both micro frontends are consuming the event library. The catalog micro frontend creates an event ADD_TO_CART in the event registry and then publishes it. The cart micro frontend subscribes to the same event and receives the count of updated products in the cart via event data. When using this technique, we also need to ensure the events subscribed to are unsubscribed once the component is unmounted.
// Catalog MFE
const Catalog = () => {
 // fetch initial count first
 const [productsInCartCount, setProductsInCartCount] = useState(initialCount || 0);
Â
 const addToCart = () => {  Â
  const addToCartEvent = new CustomEvent('ADD_TO_CART', { detail: { count: productsInCartCount + 1 } });
  window.dispatchEvent(addToCartEvent);
 }
Â
 return (
  <Product onAddToCart={addToCart} />
 )
}
Â
// Cart MFE
const Cart = () => {
 // fetch initial count first
 const [productsInCartCount, setProductsInCartCount] = useState(initialCount || 0);
Â
 useEffect(() => {
  const listener = ({ detail }) => {
   setProductsInCartCount(detail.count);
  }
Â
  window.addEventListener('ADD_TO_CART', listener)
Â
  return () => {
   event.unsubscribe('ADD_TO_CART', listener);
  }
 }, []);
 return (
  <NumberOfProductsAdded count={productsInCartCount} />
 )
};
This technique can also work for build time as well as runtime integration of micro frontends. It can work even more effectively if cross-communication is done within the same page, as events need to be subscribed to before they are published.
It’s a native javascript solution supported by most of the browsers.
Â
It’s very close to asynchronous event-based architecture in the microservices world which means it should be easy for backend developers to understand as well.
Â
It’s easy to scale.
Â
You can build a generic mechanism that all micro frontend teams can follow.
It doesn’t work for mobile micro frontends.
Â
It can cost a lot to set up.
Using a custom message bus
Â
This technique is very similar to the one above, but instead of relying on the browser’s custom events API, we build our own pub-sub mechanism.
Â
The message bus library can expose methods to publish, subscribe to, and unsubscribe from events. The publish event needs to make sure that all the subscribed handlers are invoked once the event is published.
Â
In the catalog and cart example, both the micro frontends consume the message bus library instance that is passed by the container app. The catalog micro frontend can then publish an event ADD_TO_CART, which the cart micro frontend will be subscribed to using the same message bus.
// Message bus library
class MessageBus {
 private publishedEventRegistry = {};
Â
 publishEvent(eventName, data) {
  const registeredEvent = this.publishedEventRegistry[eventName];
Â
  this.publishedEventRegistry = {
   ...this.publishedEventRegistry,
   [eventName]: {
    ...registeredEvent,
    name: eventName,
    data: data
   }
  };
  this.publishedEventRegistry[eventName].handlers.forEach(handler => {
   handler(this.publishedEventRegistry[eventName].data);
  });
 }
Â
 subscribeEvent(eventName, handler) {
  const registeredEvent = this.publishedEventRegistry[eventName];
Â
  this.publishedEventRegistry = {
   ...this.publishedEventRegistry,
   [eventName]: {
    ...registeredEvent,
    name: eventName,
    handlers: [...registeredEvent.handlers, handler]
   }
  };
 }
Â
 unsubscribe(eventName) {
  this.publishedEventRegistry = {
   ...this.publishedEventRegistry,
   [eventName]: undefined
  }
 }
}
Â
// App.js
import {Catalog} from "@mfe/catalog";
import {Cart} from "@mfe/cart";
import {MessageBus} from "@mfe-message-bus";
Â
cont App = () => {
 const messageBus = new MessageBus();
Â
 return (
  <>
   <Catalog messageBus={messageBus} />
   <Cart messageBus={messageBus} />
  </>
 ) Â
}
Â
// Catalog MFE
const Catalog = ({ messageBus }) => {
 const addToCart = () => {  Â
  // get initial count first
  const [productsInCartCount, setProductsInCartCount] = useState(initialCount || 0);
Â
  messageBus.publishEvent('ADD_TO_CART', { count: productsInCartCount + 1 });
 }
Â
 return (
  <Product onAddToCart={addToCart} />
 )
}
Â
// Cart MFE
const Cart = ({ messageBus }) => {
 // get initial count first
 const [productsInCartCount, setProductsInCartCount] = useState(initialCount || 0);
Â
 useEffect(() => {
  const listener = (event) => {
   setProductsInCartCount(event.data.count);
  }
Â
  messageBus.subscribeEvent('ADD_TO_CART', listener);
Â
  return () => {
   messageBus.unsubscribeEvent('ADD_TO_CART', listener);
  }
 }, []);
Â
 return (
  <NumberOfProductsAdded count={productsInCartCount} />
 )
};
This technique can be useful for build-time integration in the case of mobile micro frontends where there is no concept of custom events. It is important to ensure that all micro frontends are using the same instance of the message bus because an application should have only one registry for all the events published.
- It offers a custom-made solution equivalent to a message bus in a microservices implementation.
- It’s easy to scale.
- Libraries like Postal.js are available and helpful for this approach.
Like the custom events technique, it can be hard to make all the micro frontends teams follow the same pattern.
Â
It costs a lot to set up.
The choice of a particular mechanism doesn’t just depend on the micro frontend integration pattern (build time or run time) but also on the state of the project. For instance, you might not want to set the whole message bus system for an MVP as it has a huge setup cost with it. Ultimately, the best approach is to start simple and build on top of that as the project grows.
Â
To find out more about micro frontends, take a look at our webinar series from 2022.
Disclaimer: The statements and opinions expressed in this article are those of the author(s) and do not necessarily reflect the positions of Thoughtworks.