Application Shell for React Micro Frontends

Mastering Micro Frontend Integration: Achieving Cohesive UX and Optimized Performance.

Eden Ella
Bits and Pieces

--

An application shell is the “container” that integrates micro frontends together. It provides the layout, navigation, global state, and other common elements shared across the MFEs.

An app shell sets the rules for communication between the micro frontends and between the micro frontends and the shell. These rules and conventions ensure a cohesive user experience and optimized performance. They are also tremendously helpful for the development teams, as they provide a clear and consistent way to build and integrate these independent parts.

In this article, we’ll explore how to build a system of a shell app, micro frontends, and shared components using Bit’s Harmony platform. Every part in this system will be built as a Bit component that is independently developed, versioned, and delivered.

Let’s start by creating a workspace with a few of the necessary Bit components:

bit new harmony my-project --env bitdev.symphony/envs/symphony-env --default-scope my-org.my-project
cd my-project

Run the application locally to test it out for yourself:

bit run my-project
The full app running locally

Run the UI to explore the Bit components maintained in your workspace:

bit start

Your workspace should include the following components:

The Bit components maintained in the workspace are presented in the “Workspace UI”

Our workspace contains components of various types. To understand how they work together, let’s go over each one, starting with the entry point of our application, the app shell or the “platform”.

Integrating Micro Frontends and Full-Stack services with the Harmony Platform

Our app shell, pied-piper uses the Harmony Platform to compose several independent pieces that provide full-stack functionality.

Unlike most standard MFE integrations, this approach allows you to integrate full-stack services as well as “frontend-only services”.

/** @filename: my-project.bit-app.ts */

import { HarmonyPlatform } from '@bitdev/harmony.harmony-platform';
import { NodeJSRuntime } from '@bitdev/harmony.runtimes.nodejs-runtime';
import { BrowserRuntime } from '@bitdev/harmony.runtimes.browser-runtime';
import { SymphonyPlatformAspect } from '@bitdev/symphony.symphony-platform';
import { HeaderAspect } from '@bitdev/symphony.aspects.header';
import { PeopleAspect } from '@my-org/people.people';
import { PiedPlatformAspect } from '@my-org/my-project.pied-platform';

export const MyProject = HarmonyPlatform.from({
name: 'my-project',

/**
* the infrastructure that integrates the frontends/full-stack services
* among other things, it provides a way to register new routes
*/
platform: [SymphonyPlatformAspect],

/**
* the possible runtimes for this system's services.
*/
runtimes: [new BrowserRuntime(bundleConfig), new NodeJSRuntime()],

/** the frontends/e2e-services that are loaded by this app shell */
aspects: [HeaderAspect, PeopleAspect, PiedPlatformAspect],
});

export default MyProject;

Head over to the symphony platform scope, to learn more about it

The frontends or “services” integrated into the platform are defined as aspects. This is a key concept in the Harmony platform, which allows you to define and integrate full-stack services in a simple and consistent way.

Each service or “aspect” in this system provides cross-cutting functionality. For example, pied-platformallows other aspects to extend the homepage with their sections or panels, headerallows other aspects to add items to the app’s header and peopleprovides the organization’s people management functionality.

Each aspect exposes integration slots that other aspects can use to consume its services.

For example, the panelsslot is defined as follows:

/** @filename: my-project-platform.browser.runtime.tsx */

export class PiedPlatformBrowser {
constructor(
private panelSlot: PanelSlot
) {}

registerPanel(panels: Panel[]) {
this.panelSlot.register(panels);
return this;
}
static async provider(
// ...
[panelSlot]: [PanelSlot]
) {
// ...
}

export default PiedPlatformBrowser;

The peopleaspect uses this slot to register its panel:

/** @filename: people.browser.runtime.tsx */

// imports...
import {
PiedPlatformAspect,
PiedPlatformBrowser,
} from '@my-org/my-project.my-project-platform';

export class PeopleBrowser {
constructor(...) {}

/**
* each aspect lists its dependencies, which are other aspects it depends on.
*/
static dependencies = [
PiedPlatformAspect,
];

/**
* The provider method is the entry point for the aspect.
* It receives the dependencies (other aspects) it needs
*/
static async provider(
[piedPlatform]: [ piedPlatformBrowser ]
) {
piedPlatform.registerPanel([
{
category: 'People',
component: () => {
return <NewPeople />;
},
},
]);

return new PeopleBrowser(config, menuItemSlot);
}
}

export default PeopleBrowser;

Developing and Testing your Micro Frontend in its Full Context (while staying decoupled and independent)

Import the app shell (Bit component) from its remote scope into your local workspace to test your micro frontends and services in the context of the entire application.

This will provide you with an excellent dev experience similar to working on simple monolithic apps. More importantly, it will allow you to run end-to-end tests to verify your MFE is working as expected before its release.

Composing Micro Frontends and Full-Stack Services

So far, we’ve seen how the platform integrates aspects, which are essentially full-stack services. However, as mentioned earlier, our entire system is composed of Bit components that are independently developed and released.

This includes shared components of smaller granularity, such as UI components, hooks, utilities, and more.

For example, the peopleaspect is composed of several Bit components, including the people-lobby(UI) component, the use-user-listhook that fetches a list of users and even the userentity component, which is used to define the user model.

Unlike the aspects, which are more loosely coupled and provide cross-cutting functionality, these components are more tightly coupled and provide specific functionality. They are integrated or composed together as regular dependencies:

/** @filename: people.browser.runtime.tsx */

/** a bit component consumed as a regular dependency */
import { PeopleLobby } from '@my-org/people.ui.people-lobby';

These components can be used to share code between the decoupled parts of the system (micro frontends, microservices, or full-stack service) for faster development, better consistency, and easier maintenance.

The ‘people’ aspect dependencies

Run bit templates to list the available Bit component templates. Choose ‘aspect’ to create a micro frontend or full-stack service (as an aspect). For example:

bit create aspect my-header

Wrapping Up with a Few FAQs

This approach to micro frontend integration can only be used in build-time. What about runtime MFE integration?

The Harmony platform is designed to work with build-time integration of MFEs, which has become a popular approach, especially since the introduction of Bit components.

A build-time integration of Bit components is a powerful way to ensure that the MFEs are integrated in a consistent way and that they are optimized for performance and user experience.

All this can be achieved without compromising the MFEs' independence (or that of the teams that maintain them), which can be developed, versioned, and delivered independently (as Bit components).

Having said that, Bit components can also be deployed and integrated at runtime. This is a different approach that will be covered in future articles (stay tuned).

It’s also worth noting that certain elements can be integrated into the platform at runtime. For example, the following aspect will use the (aforementioned) PiedPlatformAspect and SymphonyPlatformAspectto extend the app. Here, the MFE is injected in build-time but loaded in runtime.

// imports...


/** load an MFE in runtime (in this case, an ES Module) */
export function PanelMfe() {
const HelloCard = lazy(() =>
import('https://esm.sh/@learn-bit/hello-card@0.0.2').then((module) => ({
default: module.HelloCard,
}))
);

return (
<Suspense fallback={<p>Loading...</p>}>
<HelloCard />
</Suspense>
);
}

export class MyAspect {

static async provider(
[piedPlatform, symphonyPlatform]:
[MyProjectPlatformBrowser, SymphonyPlatformBrowser]
) {
/** inject the MFE as a "panel" in the homepage */
piedPlatform.registerPanel([
{
component: () => {
return <PanelMfe />;
},
},
/** inject the MFE to the app navigation (add a route: 'my-domain/hello) */
symphonyPlatform.registerRoute([
{
path: 'hello',
component: () => {
return <PanelMfe />;
},
},
]);
]);
}
The “hello” card is loaded in runtime

How do I deploy the app shell and the micro frontends?

The app shell (“my-project”) is a Bit app component, which can be deployed to any platform using a deployment function that runs whenever a new version is released.

For example:

/** @filename: my-project.bit-app.ts */
export const MyProject = HarmonyPlatform.from({
name: 'my-project',
// …
deploy: myDeployFunction()
});

export default MyProject;

How does the platform ensure that the aspects are integrated in the right order?

The harmony platform calculates the aspect’s dependency graph to ensure that aspects are integrated in the correct order.

--

--