In Webpack V5
, the maintainers introduced module-federation
. And that's our talking point, we will dive with the parts that concerns the react developers.
Our agenda
- The Idea behind module federation
- Key elements of the plugin
- Env Setup
- Share Components
- Share Store
- Task
The Idea behind module federation
Let's say that we have a two teams working in e-commerce app specifically the search results page where the related items being rendered, let's call them A
& B
with the respected responsibilities.
A
: They are the UI people, they are responsible of UI components like the itemCard
.
B
: are responsible of getting the search results, and mapping the results into the itemsCard.
Our question is, what are our possibilities to manage these teams?
Now I need you to think of an solution before going below.
Possible Solution 1:
creating an npm package for the UI Kit where team B
can download and consume.
obviously this solution has one big drawback that when Team A
makes a new version, Team B
has to upgrade
the package manually to utilize the new version.
Not the optimal solution for our case where we need a full autonomy for each team.
Possible Solution 2:
Let's make a monorepo, where we have an app
for team B
and lazy loads the lib
made by team A
, this solution looks valid, and I had used it in my previous projects, but it has one drawback, if UI team modified the UI kit, we need to rebuild the application in order to get the newer version, since the lib is imported into the code, during build time it will be included in the site itself, okay to solve this issue.
let me introduce module-federation
.
Where we can have multiple independent sites/apps/builds and we can share code between them during runtime, this is huge because now Team A
can push changes and create new build, and when a new user comes in they will make the site on runtime
get the UI components where team A
hosted them, without team B
interference.
Key things you can achieve with module federation.
- Ability to split UI by domain, and let responsible team to take full ownership.
- A/B code Testing out of the box: where you can have two or more services and choose which to use.
- Run multiple frameworks in the same web app at once without headache, since module federation.
Plugin key elements
name
: This is the name of the application. Never have conflicting names, always make sure every federated app has a unique
name.
filename
: This is the filename to use for the remote entry file. The remote entry file is a manifest of all of the exposed modules
and the shared libraries.
remotes
These are the remotes this application will consume.
exposes
These are files this application will expose as remotes to other applications.
shared
These are libraries the application will share with other applications.
Env Setup
We will use lerna
, it is the monorepo tool of choice for most of react teams, there is also Nx, lerna uses Nx behind the scenes but lerna is simple, Nx has its own learning curve, if you would like to learn about Nx I have a dedicated series for it here
In case you have never worked with monorepos check this out
Let's go to our repo here
If you would to learn more about the set up by taking a step on step setup Please go here
Share Components
Let's explore our repo,
if you look at apps
dir, we have two react apps, host
that will contain our main app, and app1
which it will expose components into be used in in our host
.
Let's export our App.tsx for the host
, simply we need do the followings:
//apps/app1/configs/federationConfig.js
const dependencies = require("../package.json").dependencies;
module.exports = {
name: "app1",
filename: "remoteEntry.js",
exposes: {
"./App": "./src/App"
},
shared: {
...dependencies,
react: {
singleton: true,
requiredVersion: dependencies["react"],
},
"react-dom": {
singleton: true,
requiredVersion: dependencies["react-dom"],
},
},
};
Let's explain what we are doing in our case:
Shared: First we need a single instance of
react
,react-dom
, so we put thesingleton
propertytrue
. Since React, and many other libraries, have internal singletons for storing state. These are used by hooks and other elements of the React architecture. If you have two different copies running on the same page, you will get warnings and errors.name: This is a name of the build, must be unique.
filename: The name of the js file that will word as an entry point for the remotes, simply the name of the file that will let the
host
accessapp1
build.exposes: The
tsx
orts
modules that we need to share them.
Now let's set up the host federation config:
//apps/host/configs/federationConfig.js
const dependencies = require("../package.json").dependencies;
module.exports = {
name: "host",
filename: "remoteEntry.js",
remotes: {
app1: "app1@http://localhost:3001/remoteEntry.js",
},
shared: {
...dependencies,
react: {
singleton: true,
requiredVersion: dependencies["react"],
},
"react-dom": {
singleton: true,
requiredVersion: dependencies["react-dom"],
},
},
};
We have two differences between the two configs, first in host
we are not exposing, we are just consuming, in order to consume modules you need to connect into the remoteEntry
of the remote in our case our remote called app1
, the fun part is we can add as many remotes and as many exposed modules as we want.
Now we need to go to the host App.tsx
to consume the federated module:
import React from "react";
const App1 = React.lazy(() => import("app1/App"));
function App() {
return (
<>
<App1 />
<div className="App">host</div>
<div>{data}</div>
</>
);
}
export default App;
As you can see in the React.lazy
we are lazily or asynchronously importing and rendering the component, we as a host
we don't know or own the component, it is up to the service app1
team to handle it.
And for typed definitions we need to declare the federated module, since it is not found on local dirs.
To learn more I recommend the following source
//react-app-env.d.ts
/// <reference types="react-scripts" />
declare module "app1/App";
Share Store
Let's say we have a dedicated redux store for app1
and we would like to consume the store in host
.
We will not share the store itself, instead we will share the reducers
and actions
into the host
.
First from the app1
side we create a reducer.
//apps/app1/src/reducer.ts
import { createSlice } from "@reduxjs/toolkit";
type Theme = "light" | "dark";
export interface LayoutState {
theme: Theme;
}
const initialState: LayoutState = {
theme: "light",
};
const layoutSlice = createSlice({
name: "layout",
initialState,
reducers: {
toggleTheme: (state, action) => {
state.theme = action.payload as Theme;
},
},
});
export const { toggleTheme } = layoutSlice.actions;
export { layoutSlice };
export default layoutSlice;
Finally from the app1
side, we go to the federationConfig.js
inside the app1
and the add the following module:
exposes: {
"./App": "./src/App",
"./layout-slice": "./src/reducer",
}
Now we need to consume it in the host
's store:
// apps/host/src/store.ts
const federatedSlices = {
layout: await import("app1/layout-slice").then(
(module) => module.default.reducer
),
};
const initStore = async () => {
const Store = configureStore({
reducer: combineReducers({
...federatedSlices,
}),
});
return Store;
};
We are initializing the store asynchronously, we need to initialize the store before rendering, inside the bootstrap.tsx
.
initStore().then((Store) => {
root.render(
<Provider store={Store}>
<App />
</Provider>
);
});
But our implementation has its own problems, first we are waiting the app1
, to share with us the store in order to render the dom, this can go sideways in many cases,
first if app1
is down or for whatever reason did not share with us the reducer, the store will not be initialized hence the host will be down.
Special Task for the warriors
You need to figure out a way where if your app1
is down, your host
has some sorts of fallback approach and not being down for such an issue in the store.
Looking forward to learn about your solutions!
Top comments (0)