loading...
Cover image for ⚔️ Cross micro frontends communication 📦

⚔️ Cross micro frontends communication 📦

luistak profile image Luís Takahashi ・7 min read

In this article, I'm going to explain some ways to communicate between multiple applications and a particular way that I have chosen to use in my current project and work.

If you are not familiar with the micro frontends concept and architectures I suggest you take a look at these amazing articles:

There are several reasons for choosing a micro frontend architecture, maybe your app has grown too much, or new teams are coding on the same repo/codebase, but one of the most common use cases is the decoupled logic of certain domain of an App.

Following this logic, good architecture is one in which micro frontends are decoupled and do not need to frequently communicate but there are some things that micro frontends might share or communicate like functions, components, some logic, or state.

Sharing code

Draw of a package

For functions, components and common logics could be placed on a third package and imported on each app.

And for creating a package there are several approaches I won't dive deep into it, but I'll leave you some examples:

Sharing state

But what about a shared state? Why would someone need to share state between multiple apps?

Let's use a real-world example, imagine this e-commerce:
Ecommerce wireframe with the main app, cart, item details, advertising, and suggestion microfrontends

Each square represents a micro frontend with a specific domain or functionality and could be using any framework.

Ecommerce wireframe with two micro frontends pointing an arrow to the cart microfrontend

Adding some content we notice some parts of the app that might need to share some data or state like:

  • Both item detail and suggested items might need to communicate and inform the cart when an item has been added
  • The suggested items could use the current items in the cart to suggest another item based on some complex algorithms
  • Item detail could show a message when the current item is already on the cart

If two micro frontends are frequently passing state between each other, consider merging them. The disadvantages of micro frontends are enhanced when your micro frontends are not isolated modules. this quote from single-spa docs it's awesome, maybe the suggested items could be merged with item detail but what if they need to be indifferent apps?

Well for those use cases I have tried 5 different modes:

  1. Web Workers
  2. Props and callbacks
  3. Custom Events
  4. Pub Sub library(windowed-observable)
  5. Custom implementation

Comparison table

  • ✅ 1st-class, built-in, and simple
  • 💛 Good but could be better
  • 🔶 Tricky and easy to mess up
  • 🛑 Complex and difficult
Criteria Web workers Props and callbacks Custom Events windowed-observable Custom implementation
Setup 🛑 🔶
Api 🔶 💛 💛 🔶
Framework Agnostic 🔶
Customizable 🔶

Web Workers

I have created an example to illustrate a simple communication between two micro frontends with a dummy web worker using workerize-loader and create-micro-react-app also known as crma to setup the react micro frontends.

This example is a monorepo with 2 micro frontends, 1 container app, and a shared library exposing the worker.

Worker 📦

let said = [];

export function say(message) {
  console.log({ message, said });

  said.push(message)

  // This postMessage communicates with everyone listening to this worker
  postMessage(message);
}
Enter fullscreen mode Exit fullscreen mode

Container app

The container app is sharing the custom worky web worker.

...
import worky from 'worky';

window.worky = worky;

...
Enter fullscreen mode Exit fullscreen mode

You should be thinking 🤔

But why don't you import this worky on every micro frontend?

When importing a library from the node_modules and using it in different apps every worker.js will have a different hash after bundled.

worker debug

So each app will have a different worker since they're not the same, I'm sharing the same instance using the window but there are different approaches.

Microfrontend 1️⃣

const { worky } = window;

function App() {
  const [messages, setMessages] = useState([]);

  const handleNewMessage = (message) => {
    if (message.data.type) {
      return;
    }

    setMessages((currentMessages) => currentMessages.concat(message.data));
  };

  useEffect(() => {
    worky.addEventListener('message', handleNewMessage);

    return () => {
      worky.removeEventListener('message', handleNewMessage)
    }
  }, [handleNewMessage]);

  return (
    <div className="MF">
      <h3>Microfrontend 1️⃣</h3>
      <p>New messages will be displayed below 👇</p>
      <div className="MF__messages">
        {messages.map((something, i) => <p key={something + i}>{something}</p>)}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Microfrontend 2️⃣

const { worky } = window;

function App() {
  const handleSubmit = (e) => {
    e.preventDefault();

    const { target: form } = e;
    const input = form?.elements?.something;

    worky.say(input.value);
    form.reset();
  }

  return (
    <div className="MF">
      <h3>Microfrontend 2️⃣</h3>
      <p>⌨️ Use this form to communicate with the other microfrontend</p>
      <form onSubmit={handleSubmit}>
        <input type="text" name="something" placeholder="Type something in here"/>
        <button type="submit">Communicate!</button>
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Pros ✅

  • According to MDN The advantage of this is that laborious processing can be performed in a separate thread, allowing the main (usually the UI) thread to run without being blocked/slowed down.

Cons ❌

  • Complex setup
  • Verbose API
  • Difficult to share the same worker between multiple micro frontends without using a window

Props and callbacks

When using react components you could always lift the state using props and callbacks, and this is an awesome approach to share small interactions between micro frontends.

I have created an example to illustrate a simple communication between two micro frontends using crma to set up the react micro frontends.

This example is a monorepo with 2 micro frontends and one container app.

Container app

I have lifted up the state to the container app and passed messages as a prop and handleNewMessage as a callback.

const App = ({ microfrontends }) => {
  const [messages, setMessages] = useState([]);

  const handleNewMessage = (message) => {
    setMessages((currentMessages) => currentMessages.concat(message));
  };

  return (
    <main className="App">
      <div className="App__header">
        <h1>⚔️ Cross microfrontend communication 📦</h1>
        <p>Workerized example</p>
      </div>
      <div className="App__content">
        <div className="App__content-container">
          {
            Object.keys(microfrontends).map(microfrontend => (
              <Microfrontend
                key={microfrontend}
                microfrontend={microfrontends[microfrontend]}
                customProps={{
                  messages,
                  onNewMessage: handleNewMessage,
                }}
              />
            ))
          }
        </div>
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Microfrontend 1️⃣

function App({ messages = [] }) {
  return (
    <div className="MF">
      <h3>Microfrontend 1️⃣</h3>
      <p>New messages will be displayed below 👇</p>
      <div className="MF__messages">
        {messages.map((something, i) => <p key={something + i}>{something}</p>)}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Microfrontend 2️⃣

function App({ onNewMessage }) {
  const handleSubmit = (e) => {
    e.preventDefault();

    const { target: form } = e;
    const input = form?.elements?.something;

    onNewMessage(input.value);
    form.reset();
  }

  ...
}
Enter fullscreen mode Exit fullscreen mode

Pros ✅

  • Simple api
  • Simple setup
  • Customizable

Cons ❌

  • Difficult to set up when there are multiple frameworks(Vue, angular, react, svelte)
  • Whenever a property changes the whole micro frontend will be rerendered

Custom Events

Using Synthetic events is one of the most common ways to communicate using eventListeners and CustomEvent.

I have created an example to illustrate a simple communication between two micro frontends, this example is a monorepo with 2 micro frontends and 1 container app using crma to set up the react micro frontends.

Microfrontend 1️⃣

function App() {
  const [messages, setMessages] = useState([]);

  const handleNewMessage = (event) => {
    setMessages((currentMessages) => currentMessages.concat(event.detail));
  };

  useEffect(() => {  
    window.addEventListener('message', handleNewMessage);

    return () => {
      window.removeEventListener('message', handleNewMessage)
    }
  }, [handleNewMessage]);

  ...
}
Enter fullscreen mode Exit fullscreen mode

Microfrontend 2️⃣

function App({ onNewMessage }) {
  const handleSubmit = (e) => {
    e.preventDefault();

    const { target: form } = e;
    const input = form?.elements?.something;

    const customEvent = new CustomEvent('message', { detail: input.value });
    window.dispatchEvent(customEvent)
    form.reset();
  }

  ...
}
Enter fullscreen mode Exit fullscreen mode

Pros ✅

  • Simple Setup
  • Customizable
  • Framework agnostic
  • Micro frontends don’t need to know their parents

Cons ❌

  • Verbose custom events api

Windowed observable

In this new era of "micro" services, apps, and frontends there is one thing in common, distributed systems.
And looking at the microservices environment a pretty much popular communication mode is pub/subs queues just like the AWS SQS and SNS services.
Since every micro frontend and the container are at the window, I decided using the window to hold a global communication using a pub/sub implementation, so I created this library mixing two concerns pub/sub-queues and Observables, called windowed-observable.

Exposing an Observable attached to a topic to publish, retrieve, and listen to new events on its topic.

Common usage

import { Observable } from 'windowed-observable';

// Define a specific context namespace
const observable = new Observable('cart-items');

const observer = (item) => console.log(item);

// Add an observer subscribing to new events on this observable
observable.subscribe(observer)

// Unsubscribing
observable.unsubscribe(observer);

...

// On the publisher part of the app
const observable = new Observable('cart-items');
observable.publish({ id: 1234, name: 'Mouse Gamer XyZ', quantity: 1 });
Enter fullscreen mode Exit fullscreen mode

In this library there are more features like retrieving the latest event published, getting a list with every event, clearing every event, and more!

Using windowed-observable on the same app example:

Microfrontend 1️⃣

import { Observable } from 'windowed-observable';

const observable = new Observable('messages');

function App() {
  const [messages, setMessages] = useState([]);

  const handleNewMessage = (newMessage) => {
    setMessages((currentMessages) => currentMessages.concat(newMessage));
  };

  useEffect(() => {  
    observable.subscribe(handleNewMessage);

    return () => {
      observable.unsubscribe(handleNewMessage)
    }
  }, [handleNewMessage]);

  ...
}
Enter fullscreen mode Exit fullscreen mode

Microfrontend 2️⃣

import { Observable } from 'windowed-observable';

const observable = new Observable('messages');

function App() {
  const handleSubmit = (e) => {
    e.preventDefault();

    const { target: form } = e;
    const input = form?.elements?.something;
    observable.publish(input.value);
    form.reset();
  }

  ...
}
Enter fullscreen mode Exit fullscreen mode

Feel free to take a look and also use it ❤️

Pros ✅

  • Simple api
  • Simple setup
  • Pretty much customizable
  • Namespace events isolation
  • Extra features to retrieve dispatched events
  • Open source ❤️

Custom implementation

After all of these examples you could also merge some of them and create your custom implementation, using your abstractions encapsulating your app needs, but these options could be tricky and easy to mess up.

Conclusion

There is no perfect or best solution, my suggestion is to avoid hasty abstractions and tries to use the simplest solution like props and callbacks if it does not suit to your needs try the other one until it feels good!

You can dive deep in those examples in this repository.

Comment below which one you prefer and why 🚀

Discussion

pic
Editor guide
Collapse
florianrappl profile image
Florian Rappl

Really nice article - well done!

There is, however, one problem with events: You only get notified in case of changes. Especially for your problem statement (sharing some global state) you'd be interested in the current snapshot when starting. So this assumes that all interested parties are already there when the state modifier starts submitting events. This, obviously, is in general not the case. Here, you'd end up with strange situations.

A more reliable solution would be to come up with events in both directions; i.e., one to get informed about the current snapshot (originating from interested microfrontends) and one to inform about changes to the snapshot (originating from the modifier).

One problem I see with the windowed-observable lib is that it hides what it changes on window. Sure, now every microfrontend can embed this and presumably it still works (if more or less compatible versions are used) but what if somebody tries to use __shared__ for other purposes.

Thanks again!

Collapse
vbarzana profile image
Victor A. Barzana

I was gonna say this but you arrived first, good point 😉 Well, you can always come up with an unique name and document it in your overall app architecture so that no one messes up with the shared thingy. Maybe this could even be configurable in your build process as an environment variable with the build number or what not. About having a snapshot of what has happened when your micro rendered is a must, so others can track/react to already triggered events.
This was really a nice article to read. Looking forward to more of your content.

Collapse
florianrappl profile image
Florian Rappl

Unfortunately it's not so easy. The name has to be known by the microfrontends which are build and deployed independently.

Thread Thread
luistak profile image
Luís Takahashi Author

Hum... to solve it those keys/namespaces could be wrapped in another package which exposes only the communication API, and every micro-frontend uses this API

Collapse
luistak profile image
Luís Takahashi Author

@vbarzana This kind of comment made my day, I'll try to create more content for sure!

Collapse
luistak profile image
Luís Takahashi Author

I'm glad you liked it ❤️

Yep that's the main problem using the window someone may interfere in those keys 😥

Collapse
dgreene1 profile image
Dan Greene

@florianrappl it's a fair point, but at a certain point don't you have to simply trust the developers and hope that you hired the right people? Also, I think documentation, training, and on-boarding at the company should help tell people that you only communicate between microfrontends through the use of the recommended library (i.e. don't touch/read directly from window, use the library instead).

Nice work @luistak

Collapse
chimchim2 profile image
Chim

This was one of the few articles worth reading in at least month. Thanks for the work you put into this. It's something I've banged my head for the past year and you just shined light on the darkest part of using this pattern. Thank you

Collapse
luistak profile image
Luís Takahashi Author

This kind of comment really cheered me and pushed me to write more! I'm really glad you liked it, I held this article for a long time thinking it wasn't good enough.

I hope it helped at least a little in this matter o /

Collapse
jdmg94 profile image
José Muñoz

I'm going to start a migration project next month and I'm gonna use micro-frontends to approach this project, one of the things I was considering for state management is the webworker solution here! One change I would make is that I'd have a redux store on the webworker and standardize redux over both apps, great article!

Collapse
luistak profile image
Luís Takahashi Author

Hi José o/

Good luck with your project!
I don't really recommend using redux as a global state in a worker, consider sharing only the essential information

If two micro frontends are frequently passing state between each other, consider merging them. The disadvantages of micro frontends are enhanced when your micro frontends are not isolated modules.

This quote from single-spa is awesome and also try to avoid "over-reduxing"

Collapse
dgreene1 profile image
Dan Greene

I like the window observable approach you've recommended, but I think it's important to list that it has a "con" to the approach: It misses type safety.

Sure, you can change the type in the interface from T extends any to T extends unknown and that will help a little bit. But that doesn't solve the problem that occurs when a MicroFrontend falls out of date. To phrase that as a question:

What does the observing MicroFrontend (let's call her "O") do when the publisher (let's call her MicroFrontend "P") changes the structure of the event that it's publishing?

For instance:

// in P's code on Monday
obs.publish({ foo: "bar" })
// in O's code on Tuesday
const { foo } = obs.listen();

// in P's code on Friday
obs.publish({ body: { foo: "bar" } });
Enter fullscreen mode Exit fullscreen mode

What happens?

Well, MicroFrontend O is going to have a runtime error since there was no requirement to re-compile/re-deploy MicroFront O after MicroFrontend P changed it's code.

Note: Consider subscribing to me, as I'll be writing my own article on this specific problem, and I'll likely refer to yours as a starting block. Thank you for your hard work @luistak .


Enter fullscreen mode Exit fullscreen mode
Collapse
rivernotflowing profile image
River

don't see the need for any of this which will add another platform dependent part into my application. Most react applications already use either mobx or redux, they are platform agnostic. I'm sure what ever library you use on Angular or Vue should expose observable or subscription api too.

Collapse
luistak profile image
Luís Takahashi Author

Hi River!

The main message I was willing to pass was those types of sharing information between multiple micro-frontends, for sure your app may not need to do it what is also great!

Collapse
icebob profile image
Icebob

Great article!

Collapse
luistak profile image
Luís Takahashi Author

Thank you! I hope you liked it

Collapse
stokry profile image
Stokry

Nice article, thanks for sharing.

Collapse
luistak profile image
Luís Takahashi Author

You are welcome o/ 😌