Module Federation: Getting Started: The Practical Part

Introduction

This guide will walk you through the practical steps of getting started with Module Federation. We will build two separate Single Page Applications (SPAs) that use Module Federation to share components during runtime.

Setting Up the Environment

First, we need to set up our environment. We will be using a yarn mono-repo structure for simplicity. Start by creating a new project folder with the following package.json to allow us to run our two SPAs at the same time:

{
  "name": "federation-example",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "start": "wsrun --parallel start",
    "build": "yarn workspaces run build",
    "dev": "wsrun --parallel dev"
  },
  "devDependencies": {
    "wsrun": "^5.2.0"
  }
}

Next, create two directories for our SPAs to live in under a new packages directory called application-a and application-b. These will respectively contain the following package.json files:

// packages/application-a/package.json
{
  "name": "application-a",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "start": "serve dist -p 3001",
    "build": "webpack --mode production",
    "dev": "concurrently \"webpack --watch\" \"serve dist -p 3001\""
  },
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  "devDependencies": {
    "@babel/core": "^7.8.6",
    "@babel/preset-react": "^7.8.3",
    "babel-loader": "^8.0.6",
    "concurrently": "^5.1.0",
    "html-webpack-plugin": "git://github.com/ScriptedAlchemy/html-webpack-plugin#master",
    "serve": "^11.3.0",
    "webpack": "git://github.com/webpack/webpack.git#dev-1",
    "webpack-cli": "^3.3.11"
  }
}
// packages/application-b/package.json
{
  "name": "application-b",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "start": "serve dist -p 3002",
    "build": "webpack --mode production",
    "dev": "concurrently \"webpack --watch\" \"serve dist -p 3002\""
  },
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  "devDependencies": {
    "@babel/core": "^7.8.6",
    "@babel/preset-react": "^7.8.3",
    "babel-loader": "^8.0.6",
    "concurrently": "^5.1.0",
    "html-webpack-plugin": "git://github.com/ScriptedAlchemy/html-webpack-plugin#master",
    "serve": "^11.3.0",
    "webpack": "git://github.com/webpack/webpack.git#dev-1",
    "webpack-cli": "^3.3.11"
  }
}

Install the dependencies with yarn.

Bootstrapping the SPAs

Next, we need to bootstrap our SPA React applications. Create a src directory in each of our packages that contain the following files:

// packages/application-{a,b}/src/index.js
import('./bootstrap');
// packages/application-{a,b}/src/bootstrap.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
ReactDOM.render(<App />, document.getElementById('root'));

We also need a public directory in each of the packages with the following HTML template:

// packages/application-{a,b}/public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Now we can implement our two app.jsx files for each application that will house our shared components:

// packages/application-a/src/app.jsx
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

const mode = process.env.NODE_ENV || 'production';

module.exports = {
  mode,
  entry: './src/index',
  devtool: 'source-map',
  optimization: {
    minimize: mode === 'production',
  },
  resolve: {
    extensions: ['.jsx', '.js', '.json'],
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: require.resolve('babel-loader'),
        options: {
          presets: [require.resolve('@babel/preset-react')],
        },
      },
    ],
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

From the root of the application, you should now be able to access your two SPAs on http://localhost:3001 and http://localhost:3002 when running yarn dev.

Start Federating

Now that we have two independent SPAs running, let’s go ahead and make each of the SPAs a Federated Container as well as Consumer. We accomplish this by utilizing the new ModuleFederationPlugin that is part of the Webpack 5 Core.

We’ll start by adding the ModuleFederationPlugin to Application A:

// packages/application-a/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

const mode = process.env.NODE_ENV || 'production';

module.exports = {
  mode,
  entry: './src/index',
  output: {
    publicPath: 'http://localhost:3001/', // New
  },
  devtool: 'source-map',
  optimization: {
    minimize: mode === 'production',
  },
  resolve: {
    extensions: ['.jsx', '.js', '.json'],
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: require.resolve('babel-loader'),
        options: {
          presets: [require.resolve('@babel/preset-react')],
        },
      },
    ],
  },

  plugins: [
    // New
    new ModuleFederationPlugin({
      name: 'application_a',
      library: { type: 'var', name: 'application_a' },
      filename: 'remoteEntry.js',
      exposes: {
        'SayHelloFromA': './src/app',
      },
      remotes: {
        'application_b': 'application_b',
      },
      shared: ['react', 'react-dom'],
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

This specifies that Application A exposes its App component to the world as a Federated Module called SayHelloFromA, while whenever you import from application_b, those modules should come from Application B at runtime.

We will do the same thing for Application B, specifying that it exposes its App component as SayHelloFromB and whenever we import from application_a, those modules should come from Application A at runtime:

// packages/application-b/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

const mode = process.env.NODE_ENV || 'production';

module.exports = {
  mode,
  entry: './src/index',
  output: {
    publicPath: 'http://localhost:3002/', // New
  },
  devtool: 'source-map',
  optimization: {
    minimize: mode === 'production',
  },
  resolve: {
    extensions: ['.jsx', '.js', '.json'],
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: require.resolve('babel-loader'),
        options: {
          presets: [require.resolve('@babel/preset-react')],
        },
      },
    ],
  },

  plugins: [
    // New
    new ModuleFederationPlugin({
      name: 'application_b',
      library: { type: 'var', name: 'application_b' },
      filename: 'remoteEntry.js',
      exposes: {
        'SayHelloFromB': './src/app',
      },
      remotes: {
        'application_a': 'application_a',
      },
      shared: ['react', 'react-dom'],
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

The last step before we can start to utilize the exposed components is to specify for the runtime where the Remote Entries for the Containers you wish to consume are located. We do this by adding a script tag to the HTML template of the remotes you wish to consume.

// packages/application-a/public/index.html
<head>
  <!-- The remote entry for Application B -->
  <script src="http://localhost:3002/remoteEntry.js"></script>
</head>
// packages/application-b/public/index.html
<head>
  <!-- The remote entry for Application A -->
  <script src="http://localhost:3001/remoteEntry.js"></script>
</head>

The remote entry files are tiny mappings for Webpack to resolve the individually imported modules without transferring unnecessary information. They are also responsible for enabling the sharing of libraries that the packages use, in this case, when Application A requests Application B’s SayHelloFromB component, we do not send the React or ReactDOM over the wire as Application A already has a copy of it.

Consuming Federated Components

Now that our two SPA applications are Container Hosts and Consumers, we can start to consume the shared components. In the Цebpack config we had specified the names of the containers as application_a and application_b, so that is where we will import the components from.

Starting with Application A, we can render the SayHelloFromB component like so from within the bootstrap file:

// packages/application-a/src/bootstrap.jsx
import React from 'react';
import ReactDOM from 'react-dom';

import SayHelloFromB from 'application_b/SayHelloFromB';

import App from './app';

ReactDOM.render(
  <>
      <App />
      <SayHelloFromB />
  </>,
  document.getElementById('root')
);

Application B will look very similar, just importing from application_a instead:

// packages/application-b/src/bootstrap.jsx
import React from 'react';
import ReactDOM from 'react-dom';

import SayHelloFromA from 'application_a/SayHelloFromA';

import App from './app';

ReactDOM.render(
  <>
    <App />
    <SayHelloFromA />
  </>,
  document.getElementById('root')
);

Additional Notes

Looking at the network log for Application A you will see that we load two files from Application B, the remoteEntry.js file, then the 977.js that contains the SayHelloFromB component.

Visiting Application B for the first time, you’ll notice we have already cached the remoteEntries for both Application B and Application A.

Conclusion

Congratulations! You have just created your first Webpack 5 Federated projects. Now go out and build something awesome!