DEV Community

Cover image for Building Micro Frontend with React & Module Federation
Blesson Abraham
Blesson Abraham

Posted on • Updated on

Building Micro Frontend with React & Module Federation

We will be creating a production-ready micro-front end app using React, Redux, Typescript, Tailwind CSS, React Router, and Webpack but the scope of the article will be too broad and will be split up into a series

Here in this unit, we will be setting up a simple micro front end app with React, Typescript, and Tailwind CSS, and yes to simplify things, we will be using Lerna to set up a mono repo.

What's a Micro FrontEnd?

Micro frontends are similar to the microservices concept. Here each part of the WebApp you see, for instance, the Header can be in react and the sidebar can be in Angular, Vue, or any other framework. So we will have a Host/Container app that will fetch bundled codes from different URLs on load. It opens up the possibility of independent teamwork without any interdependency.

Without boring you much with the details, let's get started and will tell you the details later.

Folder Formation

Create the folders in a similar way.

- micro-frontend
   - packages
      - header
      - host
Enter fullscreen mode Exit fullscreen mode

Yes, we will be just having a header as a micro front end for now, and the host will be calling the header on load. If you are wondering why we have created these folders under packages, it's because we are using Lerna, and it's the recommended practice.

Header

Lets initialize npm in the folder.

npm init
Enter fullscreen mode Exit fullscreen mode

Now install the main dependencies.

npm i react react-dom
Enter fullscreen mode Exit fullscreen mode

Module federation is still not yet implemented in Create-React-App(CRA). So will be using webpack 5 to build the project. In CRA, under the hood, It's using Webpack but with CRA, we are completely freed up from the hustle of setting up webpack. It's not that complex to set it up if we understand what it's doing.

let's install the dev-dependencies.

npm i -D @babel/core @babel/preset-react @babel/preset-typescript autoprefixer babel-loader css-loader file-loader html-webpack-plugin mini-css-extract-plugin postcss postcss-loader style-loader tailwindcss webpack webpack-cli webpack-dev-server
Enter fullscreen mode Exit fullscreen mode

As we are using typescript to write this project, let's install the required type definitions.

npm i -D @types/mini-css-extract-plugin @types/react @types/react-dom
Enter fullscreen mode Exit fullscreen mode

Now, your package.json would look like the below.

package.json

{
  "name": "header",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "devDependencies": {
    "@babel/core": "^7.17.9",
    "@babel/preset-react": "^7.16.7",
    "@babel/preset-typescript": "^7.16.7",
    "@types/mini-css-extract-plugin": "^2.5.1",
    "@types/react": "^18.0.5",
    "@types/react-dom": "^18.0.1",
    "autoprefixer": "^10.4.4",
    "babel-loader": "^8.2.5",
    "css-loader": "^6.7.1",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^5.5.0",
    "mini-css-extract-plugin": "^2.6.0",
    "postcss": "^8.4.12",
    "postcss-loader": "^6.2.1",
    "style-loader": "^3.3.1",
    "tailwindcss": "^3.0.24",
    "webpack": "^5.72.0",
    "webpack-cli": "^4.9.2",
    "webpack-dev-server": "^4.8.1"
  }
}

Enter fullscreen mode Exit fullscreen mode

As mentioned above, We are using webpack, to limit the scope of this article, we are not gonna go into many details but will just give you a high-level overview.

What is webpack?
Webpack is a module bundler library, Which means, that when we run an npm run/serve command against a webpack project, webpack will kick in and it will read through webpack.config.js then compile and build your project using the dependencies that we mentioned in this config file. In webpack we have plugins and modules,

Loaders work at a file level, If we mention the file extension and the dependency then webpack will use that dependency to compile/transpile the files with mentioned extensions.

Plugins work at a system level. They can work on the pattern, file system handling (name, path), etc. For instance, we are using CleanWebpackPlugin, which will clean the bundle folder before generating another build.

HtmlWebpackPlugin: It will generate an HTML5 file for you that includes all your webpack bundles in the body using script tags.

MiniCssExtractPlugin: It extracts CSS into separate files. It creates a CSS file per JS file which contains CSS. It supports On-Demand-Loading of CSS and SourceMaps.

ModuleFederationPlugin: Module federation allows a JavaScript application to dynamically run code from another bundle/build, on the client and server. And here below, we expose header component.

and now that you know what is webpack, let's create the config file.

webpack.config.js

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require("./package.json").dependencies;

module.exports = {
    entry: './src/index.ts',
    output: {
        filename: '[name].[contenthash].js',
        path: path.join(process.cwd(), 'dist')
    },
    plugins: [
        new CleanWebpackPlugin(),
        new MiniCssExtractPlugin({
            filename: '[name].[contenthash].css',
        }),
        new HtmlWebpackPlugin({
            template: './public/index.html',
        }),
        new ModuleFederationPlugin({
            name: 'header',
            filename: 'remoteEntry.js',
            exposes: {
                './header': './src/Header',
            },
            shared: {
                ...deps,
                react: {
                    singleton: true,
                    requiredVersion: deps.react,
                },
                "react-dom": {
                    singleton: true,
                    requiredVersion: deps["react-dom"],
                },
            },
        })
    ],
    resolve: {
        extensions: ['.tsx', '.ts', '.js'],
    },
    module: {
        rules: [
            {
                test: /\.(ts|tsx)?$/,
                use: [{
                    loader: 'babel-loader',
                    options: {
                        presets: ["@babel/preset-typescript", "@babel/preset-react"]
                    }
                }],
                exclude: /[\\/]node_modules[\\/]/
            },
            {
                test: /\.(css|s[ac]ss)$/i,
                use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
            },
            {
                test: /\.(png|jpg|gif)$/i,
                type: 'asset/resource'
            }
        ]
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's create the react files.

index file, let's just import the Bootstrap file where we exactly do the stuff that usually gets done in the index file. Its because you might get run into an error like Shared module is not available for eager consumption

index.ts

import('./Bootstrap')
Enter fullscreen mode Exit fullscreen mode

bootstrap.tsx

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Header from './Header';

ReactDOM.render(<Header />, document.getElementById('root'));
Enter fullscreen mode Exit fullscreen mode

and one importing thing here for you to notice, We are exposing the header component through module federation and you should be importing necessary CSS in the header component, So the CSS you imported will be exposed for the exposed component and its subcomponents. Parent component CSS wont be exposed.

header.tsx

import * as React from 'react';
import "./header.scss"

const Header = () => {
  return (
    <nav class="font-sans flex flex-col text-center sm:flex-row sm:text-left sm:justify-between py-4 px-6 bg-white shadow sm:items-baseline w-full">
      <div class="mb-2 sm:mb-0">
        <a href="/home" class="text-2xl no-underline text-grey-darkest hover:text-blue-dark">Simple Header</a>
      </div>
      <div>
        <a href="/one" class="text-lg no-underline text-grey-darkest hover:text-blue-dark ml-4">Link 1</a>
        <a href="/two" class="text-lg no-underline text-grey-darkest hover:text-blue-dark ml-4">Link 2</a>
        <a href="/three" class="text-lg no-underline text-grey-darkest hover:text-blue-dark ml-4">Link 3</a>
      </div>
    </nav>
  )
};
export default Header
Enter fullscreen mode Exit fullscreen mode

header.css

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

that's it, now if you run npm serve in this folder, it will just start running on port 3001

Host

Let's create the host app and call the header app into it.

let's initiate npm

 npm init  
Enter fullscreen mode Exit fullscreen mode

and the main dependencies

npm i react react-dom  
Enter fullscreen mode Exit fullscreen mode

and now the dev-dependencies. if you notice, here we are not installing some libraries like Tailwind CSS, which is not necessary.

npm i -D @babel/core @babel/preset-react @babel/preset-typescript babel-loader css-loader html-webpack-plugin mini-css-extract-plugin postcss postcss-loader style-loader webpack webpack-cli webpack-dev-server clean-webpack-plugin
Enter fullscreen mode Exit fullscreen mode

now your package.json file might look similar below, don't miss out to add the script section to yours. It's needed for running the app.

package.json

{
  "name": "host",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "serve": "webpack serve --mode development --port 3000  --open",
    "build-dev": "webpack --mode development",
    "build-prod": "webpack --mode production"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "clean-webpack-plugin": "^4.0.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "devDependencies": {
    "@babel/core": "^7.17.9",
    "@babel/preset-react": "^7.16.7",
    "@babel/preset-typescript": "^7.16.7",
    "babel-loader": "^8.2.5",
    "css-loader": "^6.7.1",
    "html-webpack-plugin": "^5.5.0",
    "mini-css-extract-plugin": "^2.6.0",
    "postcss": "^8.4.12",
    "postcss-loader": "^6.2.1",
    "style-loader": "^3.3.1",
    "webpack": "^5.72.0",
    "webpack-cli": "^4.9.2",
    "webpack-dev-server": "^4.8.1"
  }
}

Enter fullscreen mode Exit fullscreen mode

And here below we consume the header component with the module federation plugin.

webpack.config.js

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
    entry: './src/index.ts',
    output: {
        filename: '[name].[contenthash].js',
        path: path.join(process.cwd(), 'dist')
    },
    plugins: [
        new CleanWebpackPlugin(),
        new MiniCssExtractPlugin({
            filename: '[name].[contenthash].css',
        }),
        new HtmlWebpackPlugin({
            template: './public/index.html',
        }),
        new ModuleFederationPlugin({
            remotes: {
                header: 'header@http://localhost:3001/remoteEntry.js',
            }
        })
    ],
    resolve: {
        extensions: ['.tsx', '.ts', '.js'],
    },
    module: {
        rules: [
            {
                test: /\.(ts|tsx)?$/,
                use: [{
                    loader: 'babel-loader',
                    options: {
                        presets: ["@babel/preset-typescript", "@babel/preset-react"]
                    }
                }],
                exclude: /[\\/]node_modules[\\/]/
            }
        ]
    }
}
Enter fullscreen mode Exit fullscreen mode

and lets create the react files

index.ts

import('./Bootstrap')
Enter fullscreen mode Exit fullscreen mode

bootstrap.tsx

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Host from './host';

ReactDOM.render(<Host />, document.getElementById('root'));
Enter fullscreen mode Exit fullscreen mode

Here we are importing the header and wrapping it in react suspense because we are lazy loading the header and it will show an indicator till all the children load.

host.tsx

import * as React from 'react';
const Header = React.lazy(() => import('header/header'));

const Host = () => (
    <>
        <React.Suspense fallback="Loading...">
            <div>
                <Header />
            </div>
        </React.Suspense>
    </>
);

export default Host;
Enter fullscreen mode Exit fullscreen mode

And here, we need the type definition for the header because the actual header is in another project which we are fetching via URL.

types.d.ts

declare module 'header/header' {
    export default Object
}
Enter fullscreen mode Exit fullscreen mode

now at this point, if you run npm serve in the host folder, it will just start running and would suggest you run header app before starting this, or else it would be just blank

Monorepo - Setting up lerna

Setting up Lerna is just an optional step, which has nothing to do with micro front-end architecture. Mono-repo just helps us to run/serve all projects at once without going into each folder in our local system. So you can skip this section if you don't want to include everything in a single repo.

copy the below file to your root folder(outside of your package folder) and run an npm install.

package.json

{
  "name": "root",
  "private": true,
  "scripts": {
    "serve": "lerna run --parallel serve",
    "kill-ports": "kill-port --port 3000,3001,3002,3003,3004,3005,3006"
  },
  "devDependencies": {
    "kill-port": "^1.6.1",
    "lerna": "^4.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

and create the lerna config file.

lerna.json

{
    "packages": [
        "packages/*"
    ],
    "version": "0.0.0"
}
Enter fullscreen mode Exit fullscreen mode

That's enough! Now, if you run an npm serve in the root folder Lerna will start launching each app in parallel.

GitHub Repo: https://github.com/blessonabraham/micro-frontend-react

Top comments (1)

Collapse
 
cesarpachon profile image
cesar pachon

hello what would be the code pattern if you want to conditionally rendering only one of the remotes in the host? lets say you have "settings" and "cart" views, both remote components loaded in a host app. if user goes to /settings, you want to load the host and somehow indicate to loadLazy only "settings". the same for the other. how to achieve this?