Webpack 5 Module Federation A-Z: React Foodie App (Part I) – basics 👑

Learn how to use Webpack Module Federation in real React application. Find out Micro-Frontends approach, TypeScript, AWS, Redux and many more in this blog series!

Webpack 5 Module Federation A-Z: React Foodie App (Part I) – basics 👑
Webpack 5 Module Federation part 1

Intro

Hey folks! 👋

Welcome to the Webpack Module Federation blog series!

In this blog post, I will show you a practical guide to the Webpack Module Federation with Micro Frontends Architecture.

Since there is a lot to cover, and I want to show you how to use some most popular JavaScript tools, the entire series will be divided into several parts.

This part will be a bootstrapping Module Federation project as simple as possible! At the very beginning, we will learn how to use the Module Federation and a basic overview of Micro Frontends!

Today's application will be the starting point for our React application and will be expanded in the following chapters with new functionalities and tools such as Redux, CSS in JS, Bit and configuration API with Express and NodeJS. We will also implement a ready production solution and deploy the application to AWS using GitHub Actions.

Every part of this blog series reflects git branches, so the first part will be in mf/part1 branch, second on mf/part2 and so on.

To work comfortably with this application, it would be nice if you already know about Module Federation. In case you haven't heard of it, I will explain it very briefly and give you some useful links that I recommend you check out, as this post will not explain the Module Federation concept in detail.

With that, let's start! 🔥

What is Webpack Module Federation?

Module Federation is a new concept (architecture) of JavaScript application development, which then became a new feature in Webpack as a plugin. This approach is very much related to the idea of ​​Micro Frontends:

Multiple separate builds should form a single application. These separate builds should not have dependencies between each other, so they can be developed and deployed individually.

So, the Module Federation plugin allows us to compose code from a micro-application to another at runtime.

If you still don't quite understand what this means, I encourage you to check out the links below which explain these concepts in more detail, then you'll be ready to move on to our real app example.

Webpack 5 Federation: A game changer in JavaScript architecture
Multiple Webpack builds work together, like a monolith. Painless, scaleable orchestration at runtime, on both client and
slides/ModuleFederationWebpack5.md at master · sokra/slides
Presentations. Contribute to sokra/slides development by creating an account on GitHub.

React basic application

To best understand the concept of Module Federation, is seen it in action. Especially for this purpose, I have prepared a fully functional simple application based on React. Feel free to clone the repository from the link below and check the code yourself (remember to switch onto the mf/part1 git branch):

GitHub - KrzysztofLen/react-foodie-app at mf/part1
New version of React hotel app with new approach and tech stack - GitHub - KrzysztofLen/react-foodie-app at mf/part1

Now I will cover the most important parts of this project to find out what is going on there.

Configuration:

This application is set up as a monorepo. I chose to do this mainly for the sake of simplicity, but the great power of Module Federation is that it has no problem migrating it to completely standalone modules. All can be put into their own repository, deployed and run independently.

Let's take a look on package.json in the root repository:

"workspaces": [
        "applications/*",
        "api/*" // THIS IS NOT IMPLEMENT YET
    ],
    "scripts": {
        "wipe": "rm -fr node_modules packages/**/node_modules",
        "build": "yarn workspaces run build",
        "start": "concurrently \"wsrun --parallel start\""
    },
    "dependencies": {
        "concurrently": "^7.0.0",
        "wsrun": "^5.2.4"
    }
root-package.json
💡
The application is divided into workspaces and all micro-frontend applications can be run simultaneously thanks to the concurrently package.

Applications

You can find all micro-frontend micro-applications in the applications folder, currently there are 6 of them:

micro-frontend applications in monorepo

App 1—container

App is the container for all applications. Inside it, we are consuming all other micro-frontend applications.

So let's take a look at the webpack configuration first. There are 3 webpack configuration files in the config folder:

  • webpack.common.js⁣— this file sharing configuration for both dev & prod environment, so every part of configuration which are shared is put here.
  • webpack.dev.js⁣—all the magic happens here. You can see the ModuleFederationPlugin  that uses all other micro-applications in the remote section:
plugins: [
        new ModuleFederationPlugin({
            name: 'app',
            remotes: {
                navigation: 'navigation@http://localhost:8081/remoteEntry.js',
                list: 'list@http://localhost:8082/remoteEntry.js',
                cookbook: 'cookbook@http://localhost:8083/remoteEntry.js',
                cart: 'cart@http://localhost:8084/remoteEntry.js',
                footer: 'footer@http://localhost:8085/remoteEntry.js',
            },
            shared: {
                ...deps,
                react: {
                    singleton: true,
                    requiredVersion: deps.react,
                },
                'react-dom': {
                    singleton: true,
                    requiredVersion: deps['react-dom'],
                },
            },
        }),
    ],
webpack.dev.js file inside container
  • webpack.prod.js⁣—in the prod configuration, we will configure the AWS configuration, so we can skip the analysis for now.

ErrorBoundary

Before we go any further, let's take a look at the ErrorBoundary file, which is a little more customized compared to the basic version found in the React documentation.

export class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }

    static getDerivedStateFromError() {
        return { hasError: true };
    }

    componentDidCatch() {}

    render() {
        if (this.state.hasError) {
            return (
                <React.Suspense fallback={<div>{this.props.error}</div>}>
                    {this.props.fallback}
                </React.Suspense>
            );
        }

        return (
            <React.Suspense fallback={<div>{this.props.loading}</div>}>
                {this.props.children}
            </React.Suspense>
        );
    }
}

There are a couple of props:
this.props.error⁣—we pass a loading message in case of error. ⁣this.props.fallback—failure React component in case of error.
⁣this.props.loading- loader element during loading child

I know this can be a little confusing at first, so to clarify, let's look at the order of execution:

  1. this.props.error⁣—shows during error fallback is loading
  2. this.props.fallback-shows the element when component fails and getDerivedStateFromError set the error state
  3. this.props.loading- shows when no error occurs, but normal component is asynchronously loading
  4. this.props.children⁣—render children elements

App.jsx
With this knowledge, we can look at the App.jsx file and see what is going on there. At the very top of the file, you can see the following imports:

import FallbackNavigation from './components/FallbackNavigation';
import FallbackList from './components/FallbackList';
import FallbackCookbook from './components/FallbackCookbook';
import FallbackCart from './components/FallbackCart';
import FallbackFooter from './components/FallbackFooter';
import local fallbacks inside container 

They are all dummy components—placeholders located in the /components folder only for use in the ErrorBoundary.

Next part of the more important imports are:

const NavigationLazy = React.lazy(() => import('navigation/NavigationApp'));
const ListLazy = React.lazy(() => import('list/ListApp'));
const CookbookLazy = React.lazy(() => import('cookbook/CookbookApp'));
const CartLazy = React.lazy(() => import('cart/CartApp'));
const FooterLazy = React.lazy(() => import('footer/FooterApp'));
lazy loaded Module Federated micro-applications

All this import is micro-frontend applications provided by the Module Federation. We import it on demand using the React.lazy function.

Moving on, there is a routing system that uses the React-router-dom library. All routes are wrapped in React.StrictMode for better debugging, and currently there are 3 routes in <Route /> components:  /, /cookbook, /shopping-list.

const App = ({ location }) => {
    return (
        <React.StrictMode>
            <ErrorBoundary
                error="Loading fallback navigation"
                loading="Loading navigation"
                fallback={<FallbackNavigation />}>
                <NavigationLazy items={routes} />
            </ErrorBoundary>
Part of App components with ErrorBoundary and Lazy loaded Navigation
💡
The key properties passed to the ErrorBoundary component are responsible for having only one instance of it when using the SPA. The location object comes from the HOC withRouter hook that the App wraps with.

Other apps

If we take a closer look at another applications, we can see that these are exposes in the webpack.dev configuration file:

 plugins: [
        new ModuleFederationPlugin({
            name: 'cart',
            filename: 'remoteEntry.js',
            exposes: {
                './CartApp': './src/Cart',
            },
Webpack.dev file in Cart micro-app

Most of them are very similar to each other. Only the footer is prepared as Bidirectional-host and either hosting and consuming another application:

new ModuleFederationPlugin({
            name: 'footer',
            filename: 'remoteEntry.js',
            exposes: {
                './FooterApp': './src/Footer',
            },
            remotes: {
                navigation: 'navigation@http://localhost:8081/remoteEntry.js',
            },
Footer Module Federation Bidirectional-host

Up and running
With this, you can now run the React-foodie app in two different ways.

  • All-in-one—you can run all applications simultaneously. When you go to the root of the repository in your terminal and run: yarn start, it will run each application on different ports together. You will be able to go to localhost:8080 to see the app application, localhost:8081 to see the navigation and so on …
  • Separately—you can also run all applications independently. Open each application in a separate terminal window, and run yarn start the command for every application.

In the last approach, you can observe our ErrorBoundary in action. If you don't run one of the Module Federation application, it will be replaced by fallback, and the entire application will still work. And it is amazing! 🤩

Summary:

You did it! 🎉

This is an example of the power of the Module Federation. You already know how it works with tools like react-router-dom, how to make it SPA and handling the accessibility using the ErrorBoundary feature.

This is the first part of the series that will serve as a base for future expansion with other tools, such as Typescript or Bit. So stay tuned!


Thanks for reading ♥️♥️

If this article was helpful, please leave a comment or 👍

…and stay tuned! 👊