Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
October 10, 2020 11:23 pm GMT

Cross micro frontends communication

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
CriteriaWeb workersProps and callbacksCustom Eventswindowed-observableCustom 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);}

Container app

The container app is sharing the custom worky web worker.

...import worky from 'worky';window.worky = worky;...

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>  );}

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>  );}

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>  );}

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>  );}

Microfrontend 2

function App({ onNewMessage }) {  const handleSubmit = (e) => {    e.preventDefault();    const { target: form } = e;    const input = form?.elements?.something;    onNewMessage(input.value);    form.reset();  }  ...}

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]);  ...}

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();  }  ...}

Pros

  • Simple Setup
  • Customizable
  • Framework agnostic
  • Micro frontends dont 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 namespaceconst observable = new Observable('cart-items');const observer = (item) => console.log(item);// Add an observer subscribing to new events on this observableobservable.subscribe(observer)// Unsubscribingobservable.unsubscribe(observer);...// On the publisher part of the appconst observable = new Observable('cart-items');observable.publish({ id: 1234, name: 'Mouse Gamer XyZ', quantity: 1 });

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]);  ...}

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();  }  ...}

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


Original Link: https://dev.to/luistak/cross-micro-frontends-communication-30m3

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To