Micro frontends with Module Federation and Webpack 5

ND

Nicolás Delfino / October 19, 2020

7 min read

Micro Frontends with Module Federation and Webpack 5

The release of Webpack 5 delivered something special besides performance improvements like improved tree-shaking and persistent caching across builds - an architectural possibility called Module Federation.

Module Federation (MF) enables applications to seamlessly consume and expose code in a way that hasn’t been possible before, paving the way for micro frontend composition in JS land and general federation of whatever you choose to pass through Webpack.

In this post we'll take a look into the world of Module Federation with Webpack 5 and how we can utilize this tool to create a foundation for multiple teams working on the same product.

Federated E-Commerce Application#

To demonstrate how Module Federation works, lets get started tackling our simplified e-commerce scenario once again, but this time from React SPA land.

The final application consists of a landing page with a bunch of products, a minicart and a checkout page. What's special about it is that each page is a standalone, isolated micro frontend.


Content#

  • Monorepo
  • App Shell
  • Team Checkout
  • Team Landing
  • Summary

Monorepo / Yarn Workspaces#

Although not a requirement from a Module Federation point of view, we went the monorepo route along with Yarn workspaces to facilitate running all of the separate applications simultaneously.

Yarn workspace / folder containing micro sites called sites
 "private": true,
  "scripts": {
    "installDependencies": "yarn workspaces run deps",
    "build": "yarn workspaces run build",
    "start": "concurrently \"wsrun --parallel start\"",
    "clean": "rm -fr node_modules sites/**/node_modules && yarn run clean:dist",
    "clean:dist": "rm -fr node_modules sites/**/dist"
  },
  "workspaces": [
    "sites/*"
  ],

App Shell#

The app shell is our main SPA, contains the routes, the Redux store and is the host for all of our remote applications.

Example Shell startup file
import 'team-shell/BaseStyles';
import store from 'team-shell/Store';

const Shell = () => (
  <Provider store={store}>
    <Router />
  </Provider>
);
Example routes setup - Router.jsx
const Landing = React.lazy(() => import('team-landing/Landing'));
const Checkout = React.lazy(() => import('team-checkout/Checkout'));
const Cart = React.lazy(() => import('team-checkout/Cart'));

const LandingRoute = () => (
  <React.Suspense fallback={<div className="fd_error_landing" />}>
    <Landing />
  </React.Suspense>
);
const CheckoutRoute = () => (
  <React.Suspense fallback={<div className="fd_error_checkout" />}>
    <Checkout />
  </React.Suspense>
);
const ShoppingCart = () => (
  <React.Suspense fallback={<div className="fd_error_cart" />}>
    <Cart />
  </React.Suspense>
);

const Routes = () => {
  return (
    <Router>
      <nav>
        <NavLinks />
        <div>
          <ShoppingCart />
        </div>
      </nav>
      <Switch>
        <Route path="/" exact>
          <LandingRoute />
        </Route>
        <Route path="/checkout">
          <CheckoutRoute />
        </Route>
      </Switch>
    </Router>
  );
};
Store example (using Immer.js)
const reducer = (state = { items: [] }, { type, payload }) =>
  produce(state, (draft) => {
    switch (type) {
      case 'cart/add': {
        draft.items.push(payload);
        return draft;
      }
      case 'cart/delete': {
        const { id } = payload;
        draft.items.splice(id, 1);
        return draft;
      }
      default: {
        return draft;
      }
    }
  });
App shell federation setup (webpack.config.js)
new ModuleFederationPlugin({
  name: "shell",
  filename: "remoteEntry.js",
  remotes: {
    "team-shell": "shell@http://localhost:3000/remoteEntry.js",
    "team-landing": "landing@http://localhost:3001/remoteEntry.js",
    "team-checkout": "checkout@http://localhost:3002/remoteEntry.js",
  },
  exposes: {
    "./Store": "./src/federated/store",
    "./BaseStyles": "./src/styles/federated/base.css"
  },
  shared: {
    ...deps,
    react: {
      singleton: true,
      requiredVersion: deps.react,
    },
    "react-dom": {
      singleton: true,
      requiredVersion: deps["react-dom"],
    },
  },
}),
(We'll talk about the shared prop further down...)
Recap - Shell
  • Exposes and consumes the Redux store
  • Exposes base styles
  • Handles routes

Team Checkout#

The checkout team exposes the checkout page route, the buy button and the cart:

Checkout webpack.config
name: "checkout",
filename: "remoteEntry.js",
remotes: {
  "team-shell": "shell@http://localhost:3000/remoteEntry.js",
  "team-landing": "landing@http://localhost:3001/remoteEntry.js"
},
exposes: {
  "./Checkout": "./src/federated/Checkout",
  "./BuyButton": "./src/federated/BuyButton",
  "./Cart": "./src/federated/Cart",
},
Buy button
const BuyButton = ({ payload, addToCart, children }) => (
  <button onClick={() => addToCart(payload)}>{children}</button>
);

export default connect(null, (dispatch) => ({
  addToCart: (payload) => dispatch({ type: 'cart/add', payload })
}))(BuyButton);
Checkout route example
const Checkout = ({products}) => {
  return (
    (...map products)
  )
};

const mapStateToPros = state => ({
  items: state.items
})

export default connect(mapStateToPros)(Checkout);

Checkout - Standalone#

Besides its setup to share and consume UI, checkout also exposes itself as a standalone application. This setup makes it easy for the team to develop their application and also enables them to create catalogs of the Micro frontends they own and provide to the outside world.

Standalone page using mocked product data exposed from team landing (product team)
import { products } from 'team-landing/MockedProducts';

const Standalone = () => (
  <>
    <h2>Checkout (standalone)</h2>...map products
  </>
);
Recap - Checkout
  • Exposes the checkout route
  • Buy button
  • Cart
  • Standalone

Team Landing#

Standalone landing app
Webpack.config
name: "landing",
filename: "remoteEntry.js",
remotes: {
  "team-shell": "shell@http://localhost:3000/remoteEntry.js",
  "team-landing": "landing@http://localhost:3001/remoteEntry.js",
  "team-checkout": "checkout@http://localhost:3002/remoteEntry.js",
},
exposes: {
  "./Landing": "./src/federated/Landing",
  "./MockedProducts": "./src/federated/mocks/products",
},
Example landing product page, uses buy button from team checkout
import BuyButton from "team-checkout/BuyButton"

products.map((product, index) => {
  return (
    <div className="product" key={...}>
    <div>{item.name}</div>
    <BuyButton payload={...product}>BUY - ${item.price}</BuyButton>
    </div>
    )
})
Recap - Landing
  • Exposes the landing (product) route and mocked product data
  • Uses buy button from team Checkout
  • Standalone

Dependencies#

You might have seen the shared property by now.
shared: {
  ...deps,
  react: {
    singleton: true,
    requiredVersion: deps.react,
  },
  "react-dom": {
    singleton: true,
    requiredVersion: deps["react-dom"],
  },
},

This is telling Webpack - see all of my runtime dependencies specified inside package.json as shared dependencies against others (deps), but treat libraries like React and react-dom (libraries that don't allow multiple instantiations) - as singletons. Ensuring that Webpack only loads these libraries once.

Summary#

Module federation gives us the opportunity to have multiple teams output small subsets of a site and consume them at runtime within Single Page Applications, to share UI components without NPM, to consume complete configurations, business logic etc, anything you can run through Webpack is now shareable.

The nature of MF also makes it possible to federate parts of an existing application one feature at a time, offloading responsibilities in a team manner instead of continuously adding complexity to a monolithic app.

Resources#

Source code for this post is as always available on my Github

Simplified SSR example at Module Federation Examples

Concepts & inspiration for the examples of this post comes from the only book out there today about Module Federation "The Practical Guide to Module Federation" by Jack Herrington & Zach Jackson (Zach is the creator of Module Federation). It's really well written and full with information that'll surely help you going forward with Module Federation.

module-federation.github.io

/ ND