Micro-frontends
The engineering world tries to simplify and decompose big elements to separated pieces. Big classes can be decomposed to dedicated elements using different concepts such as SOLID and others. Applications can use libraries. Backend services can be sliced to modules or micro-services.
In Frontend sphere we hear more often about the concept to decompose big Single Page Application (SPA) to modules working as micro-services, they got the name “Micro-Frontends”. We have the one orchestrator managing and playing services - micro-apps by helping to launch and connect them together.
One of the most critical aspects of micro-frontends is the communication layer.
Communication challenges
Let's consider a scenario where a user wants to use an Instagram-like application with albums, photos, and related functionality.
The user opens the main domain URL, and the browser loads the orchestrator, which then initiates the process of loading the remaining dependencies.
The application can be divided into frames or based on business logic. The second approach is more efficient for creating a pluggable system. As the user opens the first micro-app with the home page, user can see own profile with an empty album. By clicking the "Upload Photo" button, the system asks the orchestrator to mount a new micro-app, such as an uploader popup with prerequisites or context.
Let’s keep in mind the first queue of questions: how to ask orchestrator to delivery these prerequisites such as popup position, user context?
Once the user uploads a photo through the popup, the system needs to unmount the popup, refresh the profile photos, and update the album counters on the screen.
Without a long-polling mechanism or websockets, the question remains: How can we deliver an event about a new photo from one isolated micro-app to another?
Although it's a simple scenario, choosing the right concept for exchanging events between micro-apps or designing their input/outputs is the challenge.
There is no silver bullet solution, but various architectures can help address these questions such as utility modules and the centralised event manager.
Utility modules
In the context of the photo uploader module, we can identify several APIs and events, such as opening the application, uploading a photo, and closing the application. By using existing MFE frameworks like single-spa
, we can handle mounting and unmounting issues. The main challenge is to communicate between micro-apps to notify them of an event like a photo upload.
An observable mechanism can be beneficial in this scenario. We can create a simple class that encapsulates an observable, allowing us to emit events specified by an interface and subscribe to them.
export class UploaderManagerUtils {
private static newPhotoSbj = new Subject<Photo>();
public static get newPhoto$(): Observable<Photo> {
return UploaderManagerUtils.newPhotoSbj.asObservable();
}
public static createPhoto(photo: Photo): void {
UploaderManagerUtils.newPhotoSbj.next(photo);
}
}
Another visual component can import this module from my lib and handle updates:
UploaderManagerUtils.newPhoto$.subscribe((photo) => {
this.collection.add(photo);
});
And another component can trigger this event:
UploaderManagerUtils.createPhoto({ id: 1, blob: ... });
As a result, we can send uploading events with payloads without any event management systems, and all consumers can utilize them without limitations. For delivery purposes, UploaderManagerUtils
should be wrapped in an independent npm package to request webpack to import a single class instance for all micro-applications. This approach is called "Utility modules".
Pros:
- Framework-agnostic solution.
- Supports any event implementation under the hood. The utility module should support two interfaces: one to emit and one to subscribe to payloads.
- Easy to test by mocking modules.
Cons:
- Each micro-app requires an additional library to manage events, leading to logistical issues like maintaining multiple libraries, updating them for all micro-apps, and handling delivery issues.
- No specification. Theoretically, different and inefficient solutions could be used under the hood.
The centralised event manager
The micro-apps concept emphasises isolation, but at the same time, we can imagine our own specification or API for them. We can create a simple wrapper for our micro-apps, which takes all external context and uses our libraries and API to integrate internal events with the external world, and vice versa. This is achieved by passing one event manager instance that transfers events and optionally helps with state management.
While we lose some isolation because our micro-app relies completely on external parameters, we gain a robust communication layer. For example, if you want to communicate that a user uploads a photo, you don't need to create a new utility; you can just dispatch your command with the payload to the channel, and all external consumers can fetch your event. Additionally, your app can subscribe to external actions (e.g., Logout) to stop uploading properly.
The one from the most popular system helping to organise it - Redux
. It seriously was recommended by any SPA framework, has the big community, materials, solutions. Official single-spa
shares the tip to use it if we want to simplify our application.
So our app doesn’t need any additional libs and classes, I can take dispatch
function for the global store or use store API manually:
Redux
is one of the most popular systems that can help organise this centralised event manager. It is widely recommended by many SPA frameworks and has a large community, materials, and solutions. The official single-spa
guide suggests using Redux if you want to simplify your application. So your app doesn't need any additional libraries and classes, and you can use the dispatch function for the global store or use the store API manually:
this.store.dispatch({
action: UPLOADER__NEW_PHOTO,
payload: {id: 1, blob: ... },
});
After that I can subscribe to this.store
and filter actions or use reducers approach and work with my states.
Pros:
- Popular solution having many articles from React world.
- Can be easily integrate with any system.
- Provides a robust, consistent environment for all micro-apps.
- Avoiding many external dependencies.
- Easy to test my mocking events or using existing test redux frameworks.
Cons:
- Micro-app will have a bottleneck and strict relationships by using the one communication stream.
Summary
Micro-frontends have emerged as a popular approach to simplifying complex applications by breaking them down into smaller, more manageable modules. When implementing micro-frontends, the communication layer is the cornerstone for seamless interaction between micro-apps.
Consider the following solutions for efficient communication between micro-apps:
- Utility modules offer a framework-agnostic solution that supports emitting and subscribing to payloads. However, they may require additional libraries for each micro-app, leading to logistical challenges. Utility modules are ideal when you're uncertain about the future development of your product and need flexibility to balance requirements and needs.
- Centralised event managers, such as Redux, provide a consistent environment for all micro-apps and simplify communication. However, they may introduce a bottleneck and strict relationships between micro-apps. Centralised event managers are suitable when you want to focus on a single foundation and grow your product by adding new functionality, delivering modules, and providing continuous updates.
Ultimately, the choice between utility modules and centralsed event managers depends on the specific requirements and constraints of your project. Carefully evaluate your project's needs to determine the most appropriate solution for efficient communication between micro-apps in your micro-frontend architecture.
Top comments (3)
Well done! A comprehensive overview of micro frontends and various communication approaches between them.
Using the BroadcastChannel browser API is a much better approach than something like Redux, it doesn’t depend on using certain packages, or require access to shared state or variables, etc.
I don't understand anything. How can I get to know the necessary before reading about this theme?