Sharing React Components with Webpack 5’s Module Federation

Sharing React Components with Webpack 5’s Module Federation

What is the Module Federation Plugin?

Introduced in Webpack 5, the Module Federation plugin gives developers a way to create multiple separate builds that form a single application. Any JavaScript application that is bundled with Webpack 5.0 or greater can dynamically load or share code and dependencies with any other at runtime.

Why Use Module Federation?

  • Better way to share code: Expose any code from any application that Webpack supports.

  • Environment-Independent: Use shared code in different environment web, Node.js etc.

  • Resolves Dependency Issues: Federated code defines their dependencies and if Webpack can’t find it in the scope, will download it.

The Goal

We created a little shop with React, and it uses Webpack 5 as a bundler. We also have a little blog created with React.

Yup, you guessed it: it uses Webpack 5 as a bundler as well.

Inside the shop, we have a React component rendering the latest offer. But we also want to render it inside the blog! That will be our goal. We want to share the component.

First, we’ll have a closer look at both applications.

The Shop

Shop component

The shop contains one nice component - the LatestOffer component. As you can see it renders an image and some text with some fancy colours. Everything inside the orange border (and the border itself) is the LatestOffer component. It is rendered on line 8 inside BicycleShop.jsx, as you can see below:

import LatestOffer from "../LatestOffer/LatestOffer";

const BicycleShop = () => {
  return (
    <>
      <MainMenu />
      <Container style={{ padding: "60px 0 20px 0" }}>
        <LatestOffer />
      </Container>
      ...
    </>
  );
};

The component itself is defined in a separate file, LatestOffer.jsx:

import bicycleImage from "./bicycle.jpg";
import styles from "./LatestOffer.module.scss";

const LatestOffer = () => {
  return (
    <div className={styles.latestOffer}>
      <img src={bicycleImage} alt="Bicycle" />
      <div className={styles.banner}>
        <div>
          <div>Latest offer!</div>
          <div>The Jellybean 5000™</div>
        </div>
        <div>
          <div>$ 1,999.00</div>
          <div>$ 1,599.00</div>
        </div>
      </div>
      <div className={styles.shadow} />
    </div>
  );
};

And there we have it. A little shop with a LatestOffer component.

What about the other application?

The Blog

Blog page

Besides the shop, we have another standalone React application for a simple blog. Inside this blog, in the area with a dashed line around it, we want to render… the latest offer from our shop. Which is our goal, as we mentioned earlier.

We know a LatestOffer component exists, but it is defined inside the Shop repository, and not in our blog.

So how do we share the component between these applications? How do we share a component from the shop to the blog? That’s where Webpack 5’s Module Federation comes into play. It has powerful functionality, but it’s hard to grasp when you research it.

So, let’s move on and take small steps at a time.

Bundling With Webpack

Both applications (the shop and the blog) have a similar setup for bundling the code. They both use Webpack, and their configuration is for a large part very identical.

They both have the same module-rules configuration inside their webpack.config.js file, as you can see below:

module.exports = {
  mode: "development",
  ...
  module: {
    rules: [
      {
        test: /\.s?css$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              ...
            },
          },
          "sass-loader",
        ],
      },
      {
        test: /\.jsx?$/,
        use: ["babel-loader"],
        exclude: /node_modules/,
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: "asset/resource",
      },
    ],
  },
  ...
};

This makes sure the style sheets and JavaScript/JSX files are loaded and bundled correctly. But this is not a tutorial about Webpack configuration in general, so we will leave those rules for some other time.

Instead, let’s see how we can extend this Webpack 5 configuration to expose components on one end (shop) and consume them on the other side (blog).

Module Federation: Exposing Components

Our goal is to share the LatestOffercomponent from the shop application. Therefore we will modify the webpack.config.js file of the shop application first by adding an instance of the ModuleFederationPlugin . That’s how all the magic starts

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  mode: "development",
  ...
  module: {
    ...
  },
  plugins: [
    ...
    new ModuleFederationPlugin({
      name: "SHOP",
      filename: "remoteEntry.js",
      exposes: {
        "./LatestOffer": "./src/components/LatestOffer/LatestOffer",
      },
    }),
  ],
};

We’re passing an options object to the constructor:

  • name: This defines a name for the current project (remember we’re editing the Webpack configuration file for the Shop project at the moment). The name is arbitrary, and so is uppercasing it.
  • exposes: Here you define which components you want to expose, and under which name they will be made available (more about that later on). In this example, we’re exposing the LatestOffer component by giving it a name (./LatestOffer — notice the critical leading dot-slash. And this could be any name, but we decided to use the same name), and by defining the path to the component definition file as a string (./src/components/LatestOffer/LatestOffer). Needless to say, we could expose several components this way, if we wanted to.

So what about the filename, remoteEntry.js?

Other applications that want to use our exposed LatestOffer component somehow need to get hold of the component definition (the code for the component, that is). In other words, they need to be able to load that code.

Let’s say our shop application runs at the location localhost:8080 (which, coincidently, it does).

By configuring the filename remoteEntry.js (again, an arbitrary value) we created the route localhost:8080/remoteEntry.js . And you can guess what can be found there now — the code for our LatestOffer component (amongst some other boilerplate code):

remoteEntry

We have now exposed our component. We are sharing it from our shop application.

Let’s see how we can consume it on the other side. How we can render the LatestOffer component inside the blog application.

Module Federation: Remote components

We’re moving on and will have a look at the Webpack configuration file for the blog application now. Again, as we just did for the shop Webpack configuration, we will add an instance of the ModuleFederationPlugin with some options:

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  mode: "development",
  ...
  module: {
    ...
  },
  plugins: [
    ...
    new ModuleFederationPlugin({
      name: "BLOG",
      remotes: {
        SHOP: "SHOP@http://localhost:8080/remoteEntry.js",
      },
    }),
  ],
};

The options that we pass to the constructor are:

  • name: Again, a name for the current project.
  • remotes: Basically we’re telling our application, “Hey! There is this other application called SHOP, and it’s exposing components! Let’s give ourselves the possibility to load them into our project!”. The object-key, SHOP, is how we can refer locally to the external components (more about that soon). And the value is a string, constructed of two parts joined by an at-sign. The first part is the name of the remote application and the second part is the file containing the code for the components that we’re loading (it should make a bit of sense by now).

Finally, it’s time to render the LatestOffer component inside our Blog.

Let’s have a look.

Rendering Remote Components

Have a look at the Blog component that is rendering the LatestOffer component:

import React, { lazy, Suspense } from "react";

const LatestOffer = lazy(() => import("SHOP/LatestOffer"));

const Blog = () => {
  return (
    <div className={styles.blog}>
      <h1 className={styles.header}>Gerard's Blog</h1>
      ...
      <Suspense fallback={<span>Loading...</span>}>
        <LatestOffer />
      </Suspense>
      ...
    </div>
  );
};

See how we get a reference to the LatestOffer component (line 3).

We’re using a so-called dynamic import. React’s lazy function enables us to do so. It’s an advanced topic and you can read more about it in the React documentation.

After loading the (remote) component, we render it. Notice that we wrap the component inside a suspense Higher-Order Component (HOC) with a fallback value. The fallback value will be rendered while we are loading our remote component (if it was not loaded already). Once it is loaded, the LatestOffer component is rendered inside our blog!

Result

Conclusion

Webpack 5’s Module Federation has had a bit of a slow start in my opinion, and that’s a pity. This article proves how powerful it is — and believe me, we only scratched the surface.

You can troubleshoot when using Webpack5's Module Federation at: https://webpack.js.org/concepts/module-federation/#troubleshooting

Source: