Micro-FEs Simplified
Jack Herrington
Posted on December 24, 2020
Micro-Frontends, where you take large UI components and live share them between applications, have huge potential, but the mechanics of implementing them can get in the way of realizing that value. In this article, and in its accompanying video, I look at how to use Webpack 5’s built in Module Federation feature to make sharing Micro-Frontend code easy.
In fact, Module Federation makes sharing components so easy that we get to think about two follow-on issues that come with Micro-FEs:
- How to have multiple Micro-FEs from the same source share state without having the page they are hosted on implement that state sharing.
- How to all the host page subscribe or mutate the data store backing the Micro-FEs instantiated on the page.
If you want a complete walkthrough of an example three application Micro-FE demo setup where React components are shared with another React app and also a vanilla JS application check out the associated video.
This post is going to concentrate on explaining the three core concepts presented in the video in more detail.
A Little Setup
The completed Micro-FEs Simplified project contains three distinct applications relating to selling growlers.
Growlers are a PNW tradition. They are large refillable bottles we fill with beer, soda, coffee, kombucha, etc.
There is the growlers
application that has the three shared Micro-FE components. There is a Taps
component that shows all of the different beverages available for pouring into a growler. There is a Search
component that allows you to run a search on the available beverages and the results are immediately shown in the Taps
component. And then there is a Cart
component that shows this list of beverages selected as the user presses the Add to Cart
button. Shown below is the growlers
application:
On the left is the Search
component, in the middle the Taps
component, and on the right the Cart
component.
These components are then consumed in two different applications; host-react
which uses React, and host-vanilla
which uses just Vanilla JS on the page. Shown below is the host-react
:
The host-react
application shows the three Micro-FEs in a different layout and using a different Chakra-UI dark theme. In addition there is extra UI on the left hand side that is written in the host-react
that connects to the Micro-FE store and shows a more compact representation of the beverages that match the current search parameters. This deeper integration between the host page and the Micro-FEs is made possible by Module Federation.
Now that we have a better understanding of the demo application, let’s dive into the mechanics.
Using Module Federation for Micro-FEs
In Module Federation terms the growlers application is exposing modules. And you can find the mechanism for that in the webpack.config.js
file in the project. With Webpack 5 it’s as simple as importing the ModuleFederationPlugin and configuring it.
new ModuleFederationPlugin({
name: "growlers",
filename: "remoteEntry.js",
remotes: {},
exposes: {
"./DataComponent": "./src/components/DataComponent",
"./Cart": "./src/components/Cart",
"./Search": "./src/components/Search",
"./Taps": "./src/components/Taps",
"./store": "./src/store",
"./VanillaCart": "./src/vanilla/VanillaCart",
"./VanillaSearch": "./src/vanilla/VanillaSearch",
"./VanillaTaps": "./src/vanilla/VanillaTaps",
},
...
The most important fields here are the name of the federated modules container, which we specify as growlers
. Followed by the list of exposed modules. At the start we just expose the Cart
, Search
and Taps
components, as well as the store which we use to specify which client data we wish to show.
The demo app then goes on to expose a DataComponent
which React based hosts can use to show the current state of the store. As well as vanilla versions of the Micro-FE components that manage mounting each component on a specified selector (which makes it easy for vanilla JS applications to consume React components that look just like a function.
In a host application we then consume the growlers remote by using the ModuleFederationPlugin once again:
new ModuleFederationPlugin({
name: "hostreact",
filename: "remoteEntry.js",
remotes: {
growlers: "growlers@http://localhost:8080/remoteEntry.js",
},
exposes: {},
...
In this case the host-react
application is specifying that there is a remote out there, at the specified URL called growlers
.
From there, consuming and using the components is as simple as using imports:
import Search from "growlers/Search";
import Cart from "growlers/Cart";
import Taps from "growlers/Taps";
import DataComponent from "growlers/DataComponent";
import { load } from "growlers/store";
load("hv-taplist");
In this code inside host-react
we are importing the React Micro-FE components, just as we would any other React component. As well as initializing the store with our customer ID so that the Micro-FEs know what beverages data to work with.
All of this works because Module Federation is giving you the real Javascript React code to run. It’s not wrapped in a Micro-FE container. Module Federation works with any type of code that can be wepbacked; React, Vue, Angular, vanilla JS, JSON, transpiled Typescript, etc. Whatever you want.
The three key differentiators here are:
- Your Micro-FE code doesn’t need to be extracted and deployed separately from the application hosting it.
- Your Micro-FE code doesn’t need to be wrapped or bundled in any way.
- You can expose any type of code you want, not just visual components.
All of this comes with one big caveat though; Module Federation does not provide a view platform agnostic compatibility layer. It won’t help you embed a React component in a Vue application or vice versa. If you are looking for that you will want to look at something like SingleSPA (which also recommends using Module Federation as a code transport layer.) But if all your applications are React, or you are ok with something like the thin vanilla JS shims as shown in this example, then you are good to go.
Sharing State Between Micro-FEs
Since sharing code between applications is trivially easy using Module Federation the next thing our example setup looks at is how to share state between the different Micro-FEs even as they are located on different parts of the host page.
To make it even more interesting I’ll insist on the constraint that the host page should not have to implement any type of global state provider to make this work. A host application should be able to import the component and drop it on the page as-is and it should work (once the client store is specified).
To make this happen I’ll use a revolutionary new micro state manager named Valtio for two reasons. First, it’s incredibly easy to use. And second, because it doesn’t require a provider.
To set up the store in the growlers
application we simply import proxy
from Valtio and then create a store with the initial state.
import { proxy, ... } from "valtio";
import { Beverage } from "./types";
export interface TapStore {
taps: Beverage[];
searchText: string;
alcoholLimit: number;
filteredTaps: Beverage[];
cart: Beverage[];
}
const store = proxy<TapStore>({
taps: [],
searchText: "",
alcoholLimit: 5,
filteredTaps: [],
cart: [],
});
The state contains the an array of all the available beverages, the search parameters, the beverages (or taps) that match those filters, as well as the cart.
To consume the store we use the useProxy
hook in any component.
import React from "react";
import { useProxy } from "valtio";
import store from "../store";
const Cart = () => {
const snapshot = useProxy(store);
return (
<Box border={MFE_BORDER}>
{snapshot.cart.map((beverage) => (
...
))}
...
</Box>
);
};
export default Cart;
You don’t need to specify any kind of provider at the top of the view hierachy. You simply create a proxy
in a shared file, and then consume it using useProxy
.
Setting values is just as easy, we can go back to the store and look at the implementation of setSearchText
which is simply:
export const setSearchText = (text: string) => {
store.searchText = text;
store.filteredTaps = filter();
};
To set a value on a store you simply, set it. It doesn’t get a whole lot cleaner than that.
Connecting the Host Page with the Micro-FEs State
Because Valtio is so easy so use we can do even cooler things that push the boundaries of the Micro-FEs and their connection to the host page. For example we can create a novel DataProvider
component.
import React, { ReactElement } from "react";
import { useProxy } from "valtio";
import store, { TapStore } from "../store";
const DataComponent: React.FC<{
children: (state: TapStore) => ReactElement<any, any>;
}> = ({ children }) => {
const state = useProxy(store);
return children(state);
};
export default DataComponent;
Where a host page that uses React can provide a child function that renders the store state any way the host page wants. For example, the demo host-react
uses it to show much smaller beverage cards:
<DataComponent>
{({ filteredTaps }) =>
filteredTaps.slice(0, 5).map((beverage) => (
<SimpleGrid ...>
...
</SimpleGrid>
))
}
</DataComponent>
From a Micro-FE customer perspective this is great. Not only do I have ready to use Micro-FE components that I can put anywhere on the page without using a provider. And, if I don’t like the UI provided by one or more of the Micro-FEs, I have all the extension points I need to create my own components that work with the same store that’s used by the Micro-FEs.
Providing Vanilla JS Compatibility
Another issue we tackled in the video is the ability to show these components on a VanillaJS page, which is as simple as providing function wrappers around React-DOM:
import React from "react";
import ReactDOM from "react-dom";
import { ChakraProvider } from "@chakra-ui/react";
import Cart from "../components/Cart";
const App = () => (
<ChakraProvider>
<Cart />
</ChakraProvider>
);
export default (selector: string): void => {
ReactDOM.render(<App />, document.querySelector(selector));
};
Don’t be fooled by the ChakraProvider
it’s just there to provide the CSS for the components.
Then on the VanillaJS side we can simply import those functions and then invoke them on a selector:
import "./index.css";
import createTaps from "growlers/VanillaTaps";
import createCart from "growlers/VanillaCart";
import createSearch from "growlers/VanillaSearch";
import { load, subscribe } from "growlers/store";
// load("growlers-tap-station");
load("hv-taplist");
...
createTaps(".taps");
createCart(".cart");
createSearch(".search");
How are these Micro-FEs implemented? Whose to say? From the Vanilla JS applications perspective these are functions they invoke and UI appears on those selectors.
In this case Module Federation is not only handling getting the Micro-FE code to the page, but also react
and react-dom
so that the code can run. Even better, if you are lazy loading your Micro-FEs that will work just fine as well. Module Federation will bring the remoteEntry.js
file on to the page, but that file is only references to the chunks required if and when you decide to import and invoke them. So the system is inherently lazy-loadable.
Where To Go From Here
There is so much more to this example than I covered here, and to Module Federation more broadly. You can check out my playlist on Module Federation on my YouTube channel. Or you can check out Practical Module Federation, it’s a book that Zack Jackson and I wrote that covers the both the practical use, and the internal mechanics, of this fascinating new technology for sharing code.
Posted on December 24, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.