Intoroduction into Module Federation, the React Parts

ibrahimshamma99

Ibrahim Shamma

Posted on September 4, 2023

Intoroduction into Module Federation, the React Parts

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

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"],
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Let's explain what we are doing in our case:

  • Shared: First we need a single instance of react, react-dom, so we put the singleton property true. 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 access app1 build.

  • exposes: The tsx or ts 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"],
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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",
    }
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
});
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
ibrahimshamma99
Ibrahim Shamma

Posted on September 4, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related