Practical Module Federation
Practical Module Federation
FEDERATION
J ACK H ERRINGTON & Z ACK J ACKSON
This book is copyrighted © 2020, Zack Jackson and Jack Herrington, all rights are
reserved.
Preface 3
Introducing Module Federation 6
Getting Started 15
How Module Federation Works 35
Resilient Sharing of React Components 40
React State Sharing Options 49
Resilient Function and Data Sharing 75
Dynamically Loading Federated Modules 84
Shared Libraries And Versioning 93
Resilient Header/Footer 102
A/B Tests 110
Live Preview Between Applications 118
Fully Federated Sites 126
Alternative Deployment Options 139
Module Federation Dashboard 145
Frequently Asked Questions 148
Troubleshooting 151
Reference 157
Glossary 164
About the Authors 166
Module Federation is an advanced use topic for the Webpack bundler starting with
version 5. In order to get the most out of this book you should have a passing
familiarity with Webpack up to and including version 4. You should know what it’s
useful for (bundling Javascript), how it’s configured (through configuration files
written in Javascript), and how it’s extended (through plugins). This book also deals
extensively with the React view framework and with web development technologies in
general.
SETUP
You will want to have node running on your local machine at version 12 or greater to
run Webpack builds and execute the server code. You should also have git and yarn
installed for build tooling.
With this in mind, let's think about the options we have available to us today to share
that rendering code.
NODE MODULES
By far, the most common route to sharing code in the Javascript ecosystem is via an
NPM (Node Package Manager) module. In this model, either the CMS system or the
external site would extract the rendering code into a new project (and potentially a new
The simple illustration below shows the connection between these two applications and
the NPM module that holds the rendering components.
Rendering
Components
The advantage of this approach is that it’s highly controlled and versioned. To update
the rendering components, the code needs to be modified, approved, and published to
NPM. Both applications then need to bump their dependency versions and their own
versions, then re-test and re-deploy.
Another advantage of this approach is that both of the applications that consume this
library are in turn versioned and deployed as complete units, probably as a docker
image. It’s not possible that the rendering components would change without a version
bump and a re-deploy.
The primary disadvantage here is that it’s a slow process. Without automation, it’s up
to both teams to inform each other that a new version is out and request an update. It’s
also a big cognitive shift for the engineers on the project when they need to make a
change to the rendering components. Often times, it will mean that the engineer will
need to change both the host application and the rendering components in order to
close a ticket.
Another approach that has emerged in the last few years is to use a Micro-frontend
approach.
MICRO-FRONTENDS
In the Micro-frontend (Micro-FE) model, the rendering code is again extracted into a
new project but the code is consumed either on the client or on the server at runtime.
This is illustrated below as a dotted line
Rendering Micro-FE
To get this going, the Rendering Micro-FE project packages its source code as a
runtime package. The popular Micro-FE SingleSPA calls these packages “parcels”. That
code is then deployed out to a static hosting service like Amazon’s S3. The CMS
application and the external site then download this code at runtime and use it to
render the content.
The advantage of this approach is that the Rendering Micro-FE code can be updated
without either of the consuming applications redeploying. In addition, both
The downsides are that just like in the NPM model, the rendering code needs to be
extracted from the original application in order for this to work. It also needs to be
packaged in a way where it works with the Micro-FE framework, which might require
substantial modification. In the end, it might not end up looking like a run of the mill
React component.
Another downside is that an update to the rendering code might end up breaking either
of these applications. Therefore, additional monitoring will need to be added to make
this work.
Module Federation presents us with a new option that makes it far more appealing
than either of these two approaches.
Practical Module Federation Page 9 of 166
MODULE FEDERATION
With Module Federation either the CMS application or the Website, simply “exposes”
its rendering code using the Webpack 5 ModuleFederationPlugin. And the other
application then consumes that code at runtime, again using the plugin. This is shown
in the illustration below:
Rendering
Components
In this new architecture, the CMS application specifies the external website as
“remote”. It then imports the components just as it would any other React component,
and invokes them just as it would if it had the code internally. However, it does this at
runtime so that when the external website deploys a new version, the CMS application
consumes that new code on the next refresh. Distributed code is deployed
independently without any required coordination with other codebases that consume
it. The consuming code doesn’t have to change to use the newly deployed distributed
code.
- Code remains in-place - For one of the applications, the rendering code remains
in-place and is not modified.
- No framework - As long as both applications are using the same view framework,
then they can both use the same code.
Just as with any architecture, there are some disadvantages and primary among those
is the run-time nature of the linkage. Just as in the Micro-FE example, the application
is not complete without the rendering code, which is loaded at runtime. If there are
issues with loading that code, then that can result in outages. In addition, bugs in the
shared code code easily result in either or both of the consuming applications being
taken down.
A Micro-FE framework has two jobs; first, load the UI code onto the page and second,
integrate it in a way that’s platform-agnostic. Meaning that a Micro-FE framework
should be able to put React UI components on a Vue page, or Svelte components in a
React page. That’s a nifty trick and frameworks like SingleSPA and OpenComponents
do a great job at that.
Module Federation, on the other hand, has one job--get Javascript from one
application into another. This means that it can be used to handle the code loading for
a Micro-FE framework. In fact, the SingleSPA documentation has instructions on how
to do just that very thing. What Module Federation can do that a Micro-FE framework
cannot is load non-UI code onto the page, for example, business logic or state
management code. A simple way to think about Module Federation is; it makes
multiple independent applications work together to look and feel like a monolith, at
runtime.
Whether or not to use a Micro-FE framework comes down to one question; do you
want to use different view frameworks (e.g. React, Vue, Svelte, vanilla JS, etc.) on the
same page together? If so, then you’ll want to use a Micro-FE framework and use
Module Federation underneath that to get the code onto the page. If you are using the
same view framework in all your applications, then you should be able to get by using
Module Federation as your “Micro-FE framework” because you don’t need any
additional code to, for example, use external React components in your current React
application. SingleSPA also offers the ability to route between Micro-FEs, or mount
different frameworks on a single page via an opinionated and organized convention.
You would need to use one of the many great routing solutions (e.g. react-router) to get
the same features in a Module Federation system.
Managing that complexity means using the right tool for the right job. If the code
doesn’t need to be runtime shared, it shouldn’t be. An application that is entirely self-
contained and deployed as a docker image is always going to be less complex and most
likely more reliable, than one that includes runtime dependencies. Code that doesn’t
need to be runtime shared should not be until there is a requirement for it.
WHAT’S NEXT
By now, you should have a decent understanding of what Module Federation is and
how it changes the landscape of code sharing in Javascript. The chapters of the book
that follow walk through setting up Module Federation and sharing different types of
code with practical examples that you can follow on your existing projects.
The easiest way to start learning Module Federation is to try it out. To do that, we will
create two applications; home and nav. The home page application will consume and
render a Header React component that is exposed by the nav application.
Let’s start by using a pre-built boilerplate application to create the home app. I have
one specifically built for this purpose named wp5-starter-react.
First, let’s get the project directories set up. Using the Terminal, follow these steps:
% mkdir -p wp5_test/packages
% cd wp5_test/packages
% npx degit [email protected]:jherr/wp5-starter-react.git#main home
This will create a new directory called wp5_test and within it packages/home that
contains our first Webpack 5 application.
This application is pretty much the simplest possible React app that I could come up
with and it is bundled using Webpack 5.
% cd home
% yarn
% yarn start
This will automatically fire up a browser which will display this simple page.
So, let’s have a look around at the application to see how this is all put together. The
first place to look is in src/App.jsx where we have the React application itself.
import "./index.css";
In this simple React application, we create new component called App and render it on
the page.
This React application is then rendered into the HTML page contained in src/
index.html.
<html>
<body>
<div id="app"></div>
</body>
</html>
Again, not much to see, yet. To get all this running, we are using a combination of
babel to transpile the code, and Webpack to bundle the code and to run the
development server. If we look into the package.json, we can see the dependencies
and these are described below.
Dependency Description
react This is the React view library.
So far, there is really nothing new to see here. These are all standard dependencies that
should be familiar to anyone who has written webpacked React applications. The
important element here is that all of these dependencies are at the latest version, and in
particular, that Webpack has a version of 5.0.0-beta.18 or higher.
At the top of the file, we bring in the HTMLWebPackPlugin which uses the HTML
template in src/index.html to create the web page that hosts the React application.
The rules section in modules tells Webpack how to identify and interpret the
different types of source files based on the extension.
Field Description
name This is the name of the application. Never have conflicting
names, always make sure every federated app has a unique
name.
filename This is the filename to use for the remote entry file. The
remote entry file is a manifest of all of the exposed modules
and the shared libraries.
remotes These are the remotes this application will consume.
exposes These are files this application will expose as remotes to other
applications.
shared These are libraries the application will share with other
applications.
- Remotes - These are the names of the other federated module applications that this
application will consume code from. For example, if this home application consumes
code from an application called nav that will have the header, then nav is a “remote”
and should be listed here.
- Exposes - These are the files that this application will export as remotes to other
applications.
- Shared - This is a list of all the libraries that this application will share with other
applications in support of files listed in the exposes section. For example, if you
export a React component, you will want to list react in the shared section because
it’s required to run your code.
Now that we understand the project and how it works, it’s time to start making some
modifications.
{
"name": "home",
...
new ModuleFederationPlugin({
name: "home",
filename: "remoteEntry.js",
...
}),
Now, we are ready to consume some federated modules and put them on the page, but
in order to do that, we need to have some. So, let’s create a new project called nav that
will have a Header component we can consume.
% cd wp5_test/packages
% npx degit [email protected]:jherr/wp5-starter-react.git#main nav
If you like, you can remove the index.css and App.jsx files as well, we don’t need
those.
new ModuleFederationPlugin({
name: "nav",
filename: "remoteEntry.js",
remotes: {},
exposes: {
"./Header": "./src/index",
},
shared: { ... }, // Shared React dependencies
}),
The exposes object has pairs of keys and values for all of the different Javascript files
this application exposes to other applications. In this case, we are saying that ./src/
index.js is exposed as Header to any consuming application.
We are adding react to the list of shared modules because our component relies on
React. If you add more dependencies to your application that are used by the Javascript
The final thing we need to do is specify the port number for our development server.
We do that by altering the the port numbers in webpack.config.js:
module.exports = {
output: {
publicPath: "http://localhost:3001/",
},
...
devServer: {
port: 3001,
},
...
};
We need to do this because when we reference this nav application in our home
application, we need the location to be a known value.
We also need to modify the webpack.config.js to let Webpack know the base
location for the server code and pages from the nav app. This is an important step,
because this will be used to asynchronously load code at runtime. If the origin is
different from the consumer, the remote is unable to know where it should download
its own code from.
plugins: [
new ModuleFederationPlugin({
name: "home",
filename: "remoteEntry.js",
remotes: {
nav: "nav@http://localhost:3001/remoteEntry.js",
},
exposes: {},
shared: { ... }, // Shared React dependencies
}),
To do that, we add a nav key/value pair to the remotes key in the Module Federation
options. The key is what you would like to call this remote internally. The value is the
name of the remote specified in the remotes Webpack configuration. For simplicity,
let’s use nav for both of these.
- The incoming Header component would download its own shared version of
react. Since React is very large, that would significantly add to the weight of the
page.
- React, and many other libraries, have internal singletons for storing state. These are
used by hooks and other elements of the React architecture. If you have two
different copies running on the same page, you will get warnings and errors.
import "./index.css";
At the top of the file, we import Header using a React lazy import. In the body of the
component, we reference that header within a React.Suspense object. We also
specify that an empty <div> tag should be used until the content loads.
With that all set up, we can run yarn start in the home directory within a new
terminal session (both servers need to be running). The result should look something
like what is shown in the figure below.
MAKING A WORKSPACE
Now that we have our example up and running, we should spend a second to make it
easier on ourselves by creating a workspace in the wp5_test directory. First, stop both
of the development servers and remove the node_modules directories and
yarn.lock files.
% cd wp5_test
% yarn init -y
After that, install the concurrently and wsrun packages in that package.json
using the yarn add command.
"private": true,
"scripts": {
"start": "concurrently \"wsrun --parallel start\""
},
"workspaces": [
"packages/*"
],
The private key and workspaces keys tell yarn that we are creating a monorepo
project that hosts multiple packages within a single repository. Those packages are
contained in a packages subdirectory we are going to create.
The start script uses wsrun (workspaces run) and concurrently to execute both of
the servers simultaneously.
Nav
module.exports = {
output: {
publicPath: "http://localhost:3001/",
},
plugins: [
new ModuleFederationPlugin({
name: "nav",
fileName: "remoteEntry.js",
exposes: {
"./Header”: "./src/Header"
}
}),
],
};
Home
src/App.jsx
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "home",
remotes: {
"my-nav": "nav@http://localhost:3001/remoteEntry.js"
}
}),
],
};
The publicPath is the base of the remote URL which ends with the fileName. And
then before that is the remote name defined using name in the Module Federation
...
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
"react-dom": {
singleton: true,
requiredVersion: deps["react-dom"],
},
},
They are marked as singletons because those two libraries; react and react-dom
have internal state and you can’t have multiple copies of them on the same page. That
qualifies them a “singletons” and telling Webpack this ensures that it will never load
more than one copy of react, or react-dom on the page at one time.
The value of the spread of the deps object (i.e. ...deps) is that all of the other
runtime dependencies for the application are added to the list of shared libraries
automatically. So if you bring in antd, or lodash, those will be automatically shared.
shared: {
...
"@emotion/core": {
singleton: true,
requiredVersion: deps["@emotion/core"],
},
},
Any time you have globally shared state within a module, which is very easy to do, then
your library becomes a singleton.
The different modes for the shared key are explained in more in the appendices at the
end of the book. But if you are using React then we strongly recommend adopting this
standard pattern.
WHAT’S NEXT
Now that you’ve got your first experience with Module Federation, it is time to dig into
the details of how to share code between applications safely. That starts with a deep
dive into the world of sharing React components.
Now that we understand where Module Federation fits into the world, and we’ve tried
it out on a very basic application, let’s increase our depth of knowledge by exploring
how Module Federation works.
BUILD TIME
The ModuleFederationPlugin we’ve been using thus far is really just syntactic
sugar over three interrelated plugins--it serves as a convenience plugin that passes
options to the separated parts that perform the heavy lifting. You can use each of those
individually and customize how you see fit so that you have ultimate control over the
output.
- ContainerPlugin - This plugin manages the export the remote modules that you
define in the exposes key of the configuration. It also creates the remote entry file
that acts as a manifest for the application, which is called the “scope” internally. If
your application only exposes remote modules, then this is the only plugin you need.
This is what a remote uses.
To get a deeper look at what’s built by Webpack, a good way to do that is to run the
build in development mode and to look at the output of the dist directory, like this:
In an example project that exposes one remote module, Header, the example output is
shown below:
Asset Size
index.html 171 bytes [compared for emit]
main.js 2.93 KiB [emitted] [name: main]
remoteEntry.js 12.1 KiB [emitted] [name: nav]
src_header_js.js 2.3 KiB [emitted]
vendors-node_modules_react_index_js.js 73.7 KiB [compared for emit] [id hint: vendors]
Entrypoint main = main.js
Entrypoint nav = remoteEntry.js
- index.html - This is the compiled HTML file that includes the main.js file which
runs the application.
- main.js - This is the compiled code for the main entry point for the application.
It’s important to understand that these files are a fusion of the Javascript bundles
required to run the application itself, as well as the Javascript bundles required for the
remote modules. Often times those two overlap. For example, the same vendor bundles
that are referenced by the remote modules are used by the application itself.
Now that the Javascript is all compiled, let’s discuss how it actually works at runtime.
For example, you have an application with the name of nav that exposes a runtime
module named Header. After loading the code, you would be able to open up the
console and inspect window.nav and see that it has two functions, get and
override.
window.nav.get('Header')
This returns a promise, which when resolved gives you a factory. We can invoke that
like this:
This will output through console.log the module as defined in the Header
implementation. If Header exports a default, the value returned from factory()
will be an object with a key named default. If the module has other named exports,
those will also be attached to the object.
A side effect of running the factory is also to load any shared packages required by the
remote module. Webpack is good about only loading the shared packages required to
support each module. For example, if component A imports react and lodash, but
component B only imports react, running the factory on B would only bring in
react.
WHAT’S NEXT
Now that we’ve dug more deeply into Module Federation and how it works, we will
jump back into the world of practical implementation and cover how to safely export
and consume React components in a federated context.
Let’s visualize the connection between the home app and the nav app we created in the
last chapter. The home app has a runtime connection to the Header.
Header
However, any runtime connection can be broken. Let’s try that out by simply starting
the home server without running the nav app. The result is that the page is broken. No
content is rendered at all. In the console we see that nav is undefined:
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
It’s unfortunate that we can’t use a hook for this, but React does not currently support
hooks for error boundaries. The good news is that the page renders and we get our
error message instead.
Shown below is the code for FederatedWrapper that handles both a slow load and an
error during component execution.
render() {
if (this.state.hasError) {
return this.props.error || <div>Something went wrong.</div>;
}
return (
<React.Suspense fallback={this.props.delayed || <div />}>
{this.props.children}
</React.Suspense>
);
}
}
To use this wrapper component, all you need to do is wrap the Header component in
the FederatedWrapper with messaging alternatives for the error and delay
properties.
This means that we can then create the Header component using this wrapper:
This creates an API for our React components, that we import via Module Federation,
that’s both easy to use and resilient to errors.
BOOTSTRAPPING APPLICATIONS
Another important safety tip is to “bootstrap” your application. Bootstrapping means
that your main entry point should be to a file whose job is to asynchronously load the
main application. For example, you would have an index.js file, which is the main
entry point for Webpack, and its contents would be:
import("./App");
The local App.jsx file would have the application, like so:
A non-bootstrapped version of this application would have an index.js file with the
contents:
And since index.js is the main entry point of the application (which is the default for
Webpack) then this code will execute immediately and not give Webpack any time to
load the required remotes before execution.
Then Webpack would not have the opportunity to load the nav remote before that code
executes, and it would result in a runtime error.
Code for this chapter is available on GitHub. This chapter has sub-directories for each of the different
state management libraries.
Once you have a way to share code between applications, whether that be through NPM
modules, Micro-FE frameworks, or through Module Federation, the inevitable next
conversation that arises is how to share state between the host and the remote
modules, or even between remotes. This chapter explores some options on how to
share state.
new ModuleFederationPlugin({
...
shared: [],
}),
Now, let’s break this application by making one small change to the header.
The original Header was stateless, but just by adding a useState hook the Header
now throws an error and we can see this message in the console logs:
Uncaught Error: Invalid hook call. Hooks can only be called inside of the
body of a function component. This could happen for one of the following
reasons:
1. You might have mismatching versions of React and the renderer (such as
React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
This error is telling us what we already know; there are two react library instances on
the page. However, we didn’t get the error until we tried to use hooks. That’s because
It’s important to understand this behavior, and the importance of sharing all of your
runtime dependencies for this reason: any state sharing library is likely to have global
state or use features of the framework that require global state. You always need to
share these libraries.
To fix this we use this configuration when we share react and react-dom.
...
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
"react-dom": {
singleton: true,
requiredVersion: deps["react-dom"],
},
},
This defines react and react-dom as singletons which tells Webpack to make sure
that there is never more than one copy of react or react-dom on the page at a time.
With some understanding of how these libraries can go right and wrong, it’s time to dig
into some specific state sharing options.
To do that, let’s first add the react library back into shared in both projects and then
re-launch if you haven’t done that already. Then let’s alter the App.jsx in home to add
some new functionality.
This new version of the App component has its own state that contains a cart count.
After pressing the “Buy me!” button five times, the result in the browser looks like the
figure below:
The result is that we now have a Header from the nav application that tracks with the
cart count state that’s stored in the home application.
It’s not surprising this worked. This is the simplest and most straightforward way to
share state in React. However, it does provide a solid baseline to compare with some
other approaches.
REDUX
If you’ve used the React framework, you’ve heard of Redux. It’s easily the most
commonly used state manager. To see how Redux works in the Module Federation
context, we start with simple project we finished in chapter four. From there, we add
redux and react-redux to home and nav applications.
% cd packages/home
% yarn add redux react-redux
% cd ../nav
% yarn add redux react-redux
From there, we go into the App.jsx file and implement a new Redux store.
This very simple Redux store has an initial state with a count of zero. There are two
actions: INCREMENT will add one to the count and RESET will set the count to zero.
Then we change the ReactDOM.render invocation to wrap our App in a Provider with
the store.
This is a critical step because this Provider is how we are going to get the state to any
of the consumers. The big question is whether it will work from the home application
through Module Federation into the nav app.
Before we get there, we need to adjust the App React component to use this store.
The connect function, provided by React-redux, is a high order function that takes the
App component and subscribes it to the store. It extracts fields from the state and
The component now looks at count, which connect takes from the store and sends to
the component as a property. The event handler uses the dispatch function to send the
INCREMENT action back the store.
The only thing left to do is update the Header so that it connects to the store properly.
Let’s head over to the nav application and alter the App.jsx file to look like this:
It works just fine. It turns out that you can access your Redux store seamlessly in React
components integrated using Module Federation.
% cd packages/home
% yarn add mobx mobx-react
% cd ../nav
% yarn add mobx mobx-react
Then we create the store, which is as easy as adding this code to the App.jsx file:
We then pass the store to the App component by re-writing the ReactDOM.render
invocation.
...
We’ve removed the state from App and now we just use the count as a regular
Javascript object. To set it, all we need to do is just set its value and the observable
handles all the rest. We also need to send the store to Header as a property.
Once again, we display the data simply by showing the value. We reset the value just by
setting it to zero. This all works seamlessly across these components brought together
through Module Federation.
RECOIL
Recoil is a novel state management system from FaceBook that is designed to extend
the existing useState and useContext mechanisms by providing state that is
“orthogonal to the rendering tree”. Meaning that useState and useContext relate
directly to where they are used in the React tree. When useState or useContext
change anything below where they are defined in the React tree, will be re-rendered.
Recoil, on the other hand, stores its state as “atoms” that are held apart from the tree.
When an atom changes state then only those components that are using it will re-
render.
% cd packages/home
% yarn add [email protected]
% cd ../nav
% yarn add [email protected]
At the time of this writing, only version 0.0.7 of recoil was compatible with this
approach. With that done, we can launch the servers with yarn start. Then we
create a new file in the src directory of the home app called atoms.js with these
contents.
This creates a new “atom” that holds the cart count value. In the recoil model, state is
stored as atoms and components subscribe to those atoms. If an atom's value changes
then any component that subscribes to it will be updated. You can have as many atoms
as you like. You can use another mechanism called a “selector” to create composite
atoms from a collection of other atoms.
ReactDOM.render(
<RecoilRoot>
<App />
</RecoilRoot>,
document.getElementById("app")
);
One of the great things about Recoil is how it’s almost a plug and play replacement for
useState. What do you do if more than one component need the state? Make an atom
and point both components at it by just replacing useState with useRecoilState.
It’s very clean.
new ModuleFederationPlugin({
...
exposes: {
"./atoms": "./src/atoms",
},
...
}),
Then over in the nav application, we need to alter the webpack.config.js to add
home as a remote.
new ModuleFederationPlugin({
...
remotes: {
home: "home@http://localhost:8080/remoteEntry.js",
},
...
}),
This creates a bi-directional linkage between home and nav where the home
application consumes the Header from nav and the nav application consumes
atoms from home. And it all works!
It’s as easy as that. When the user clicks on the button, the atom is updated and all the
consumers of the atom are updated automatically.
RXJS
RxJS is a very popular event bus library. It’s not often used as a state mechanism, but
there is no reason why it can’t be done.
% cd packages/home
% yarn add rxjs
new ModuleFederationPlugin({
...
shared: { ... }, // Shared React dependencies including rxjs,
}),
Though this is optional in this case because home will be creating RxJS subjects which
are self contained observable objects that don’t require additional methods from the
library to use them.
The next thing to do is to import Subject from rxjs and to create a Subject that will
hold the cart count.
Subjects in RxJS are event busses. And in this case we are starting the topic off with the
value of zero. You can post a new value by using the next method on the object. And
you can subscribe to the bus by calling to subscribe method.
This hook takes the subject and its initial value and uses a standard useState to hook to
track its state. It then subscribes to that subject and updates the state any time it
changes.
return (
<div>
<React.Suspense fallback={<div />}>
<Header count={count} onClear={() => count.next(0)} />
</React.Suspense>
<div>Hi there, I'm some cool product.</div>
<button onClick={onAddToCart}>Buy me!</button>
<div>Cart count is {itemCount}</div>
</div>
);
};
We use the useSubject hook we just created to get the current value of items in the
cart. And then we use the next method on the count subject to increase its value, or set
it to zero in the case of the onClear.
Now we could just send itemCount to the Header, but instead let’s send the subject
so that it too can subscribe to it. To do that we change Header.js in the nav project.
The result is shown in the code fragment below:
And we reuse the same useSubject code from home, which we could share via NPM
module or via Module Federation.
To set that up we first create a new file in home named analytics.js with these contents:
exposes: {
"./analytics": "./src/analytics",
},
That allows us to import it using the name home, like this in App.jsx.
It’s important that we import it by referring to home to ensure that both the App
component and the Header component get exactly the same analytics subject.
Now that we have it imported we can attach a console function to it using subscribe.
analyticsBus.subscribe((evt) => {
console.log(`analytics: ${JSON.stringify(evt)}`);
});
<button
onClick={() => {
analyticsBus.next({ type: "onClear" });
onClear();
}}
>
Clear
</button>
Using RxJS for an analytics event bus in a way that’s more in-line with its original
purpose. It also opens up the opportunity to put loggers, filters, mutations and much
more into the bus to work the analytics events in whatever way you please. RxJS
provides a lot of tooling for that.
HOW TO CHOOSE
The choice of a state manager is highly dependent on the type of application you're
building. Here are some recommendations:
For an internal dashboard, where the modules will pick and choose between a variety
of data, then Recoil seems like an interesting solution because it would limit the re-
renders to just those components subscribed to the specific atom that is being updated.
An e-Commerce application, or any application that has very limited data sharing
requirements (user identity, cart contents, etc.) between federated modules using
either a few Recoil atoms or a small Redux store would work well.
There are many state managers in the React ecosystem and this chapter has only
covered a few. If you're starting with a new project, you should develop a set of
requirements and then evaluate and run proof of concepts of several solutions so you
can evaluate how well they match your requirements and how the developer experience
feels.
One of the great parts about Module Federation is that it’s not boxing you into any
particular state management solutions. The industry standard solutions that work in
your non-federated React application work just as well in a federated application.
Code for this chapter is available on GitHub. This chapter has sub-directories for each of the different
state management libraries.
One of the most unique and important aspects of Module Federation is that you can
share anything that can be represented as a Javascript module. This includes:
To emphasize this point, we’ll walk through some examples and show how they are
exported and consumed, and also show different ways to manage them safely.
new ModuleFederationPlugin({
name: "logic",
filename: "remoteEntry.js",
remotes: {},
exposes: {
"./analyticsFunc": "./src/analyticsFunc",
"./arrayValue": "./src/arrayValue",
"./classExport": "./src/classExport",
"./objectValue": "./src/objectValue",
"./singleValue": "./src/singleValue",
},
shared: [],
}),
This is then consumed by a home project that works with the different types of data.
PRIMITIVE VALUES
Let’s start with the simplest case, the single value. Looking at the singleValue.js
file we see it implemented as just a default return of a string.
React.useEffect(() => {
import("logic/singleValue")
.then(({ default: value }) => singleValueSet(value))
.catch((err) => console.error(`Error getting single value: ${err}`));
}, []);
This code creates some state to hold the value using a useState hook. Then it uses the
useEffect hook to start the import. The import can either return successfully, in
which case we get back an object with the default key that we map to value and
then use to set the singleValue. In case of an error, we send that error to the console.
At the end, we render the result into a <div> tag in our application. The result looks
like the screenshot below.
What this shows is that importing values, of any type, just requires an asynchronous
import to bring in safely. You can use synchronous imports in some cases, but to build
a resilient code base you should import the code asynchronously and handle the case
when you are unable to get the code you need.
FUNCTIONS
It starts to get more interesting as we look at functions. Let’s take an example of an
analytics sending function. The example code defines it like this:
Here is the interesting thing about promises, they will resolve immediately if the
promise has already been resolved. In this case, you can send as many calls as you like
to this function and once it resolves the first time it will unlock the rest and send
everything out.
If you want something more generic, you can create a function that takes an import
with a function as the default and returns a function that you can call in the same way
as sendAnalytics above.
if (!pending) {
pending = true;
funcPromise
.then((func) => {
queueFunc = func;
queue.forEach(queueFunc);
queue = [];
})
.catch((err) => console.log(`Error getting queued function`));
}
}
};
};
This function takes an asynchronous import promise, just like the one above.
However, until it resolves, it maintains a queue of messages that it then sends to the
function using the forEach.
CLASSES
The last thing to try out is a non-React class. The test class in the logic application is
defined this way.
class MyClass {
constructor(value) {
this.value = value;
}
logString() {
console.log(`Logging ${this.value}`);
}
}
This code starts the import and gets a promise back. Then it creates a function that
uses that promise and either throws an error if it catches or, if it works, returns a new
object with the values it’s given as arguments.
Exciting stuff! This shows that you can safely consume and create objects from classes
exported from remote modules.
- A/B testing code - Code for different test variants that you can load on the fly
using asynchronous imports
- Feature flags - Javascript objects containing feature flags or settings
- i18n strings - The localization strings for different languages, lazily loaded
- Persistent state management - Javascript code to manage and store persistent
state in local storage on the client
WHAT’S NEXT
Now that we know how Module Federation works and how to export and import any
kind of code safely, the next chapter will cover how to load Federated Modules
dynamically.
Code for this chapter is available on GitHub. This chapter has sub-directories for each of the different
state management libraries.
So far, the relationship between our host applications and our remotes has been fixed.
For example, the home page includes a reference to the nav app where it gets the
remote Header module. But you don’t have to do this. You can dynamically load the
remoteEntry.js onto the page and then execute your own version of import to run
the code.
- widget - This is a single React component that is shared as a remote module in the
scope of widget and under the name Widget.
- home-wp5 - This is a host application, running on Webpack 5, that dynamically
loads and runs widget.
- home-wp4 - This is a host application, running on Webpack 4, that dynamically
loads and runs widget.
new ModuleFederationPlugin({
name: "widget",
filename: "remoteEntry.js",
remotes: {},
exposes: {
"./Widget": "./src/Widget",
},
shared: { ... }, // Shared React dependencies
}),
The important elements here are that we export the Widget component and that the
name of this application is widget. It’s also good to see that we are running and
exporting this remote module off of port 8082.
output: {
publicPath: "http://localhost:8082/",
},
devServer: {
port: 8082,
},
The React widget itself is super simple as shown in the code below.
Over in the home-wp5 directory, we have the host for the Widget remote module. If we
have a look at its webpack.config.js, we can see it only uses
ModuleFederationPlugin to share react, other than that, this is just a standard
Webpack 5 configuration.
function System(props) {
const { ready, failed } = useDynamicScript({
url: props.system && props.system.url,
});
...
The useDynamicScript hook is a custom React hook that, given a URL, in this case
for the remoteEntry.js, adds a script tag to the page and then sets ready to true
when the script is loaded (or failed to true if that failed).
This function is doing what import does. It’s using the scope (widget) and module
(Widget) to get the module factory from the globally mounted object. Then it invokes
that factory and returns the module. This is just what React.lazy wants to see.
The reason that react is called out in particular is that React is a singleton that requires
that there is only a single instance on the page at a time. So this code initializes the
scope with the host pages React library instance so that the widget module avoids
loading its’ own version.
The result when we run yarn start in the home-wp5 directory (while the server is also
running in widget) looks like this.
However, what happens if you aren’t on Webpack 5. Can you still do this?
"devDependencies": {
...
"webpack": "^4.43.0",
...
},
The webpack.config.js file for this project has no mention of Module Federation as
it was not available in version 4.
In the src/App.jsx file, we can see the implementation for the host React
component. It has the same useDynamicScript hook that we used in the Webpack 5
version just to get the code onto the page.
...
window[scope].override(
Object.assign(
{
react: () => Promise.resolve().then(() => () => require("react")),
},
global.__webpack_require__ ? global.__webpack_require__.o : {}
)
);
return (
<React.Suspense fallback="Loading System">
<Component {...props} />
</React.Suspense>
);
};
The React.lazy and React.Suspense are basically the same as in the Webpack 5
example. The big difference is that we are manually setting the overrides for the
Once that’s all set up, we can bring in the Widget from the widget application just
like any other React component.
It’s a testament to the stability of the Webpack architecture that we can use cutting
edge features from one version reliably in the previous version.
WHAT’S NEXT
In the next chapter, we dig into how to share packages properly across federated
modules.
If you take a look at any Javascript file in your application, it’s probably going to start
with at least a few, if not a lot, of import statements, many using node modules or
packages. That’s just the nature of the Javascript ecosystem. It does make for a
challenge when we share code because the piece of code that brings in those imports
needs those packages, and needs them at the right versions.
- Duplication - Without sharing the packages, you get duplication and this will slow
down the experience.
- Version mismatch - If we don’t get the sharing setup correctly, then different
remote modules could end up consuming incorrect versions, which could lead to
crashes.
- Singletons and internal state - Many libraries, particularly frontend libraries,
have internal state that is required in order to run properly. That means there can
only be one instance of that library running at a time. We call this a “singleton”.
You can list the libraries to share between host and remote as easily as this:
shared: ["lodash"],
This chapter walks through the different ways to configure the shared key with
recommendations on when to use each of the different options.
SPECIFYING VERSIONS
There are three ways to specify shared versions. The first way is the array syntax we’ve
been using thus far with the names of each of the libraries to share. The second way is
an object syntax with the name of the library as the key, and the version (using
semantic versioning format) as the value. It looks like this:
"shared": {
"react": "^16.12.0",
"react-dom": "^16.12.0"
}
If that looks familiar, that’s because it looks exactly the same as it does in
package.json and it works exactly the same way. In fact, because
webpack.config.js is just a Javascript script you could do:
"shared": require('./package.json').dependencies
However, you can be even more precise about the control by using the third way, where
you hint Webpack in exactly how to manage the shared dependency.
FALLBACK
Webpack has a fallback behavior if the package provided as shared doesn’t match the
requirements specified in the ModuleFederationPlugin or SharePlugin
configuration.
This fallback feature enables versioning deployments at scale because not all
applications will be required to push new versions at the same time.
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
"react-dom": {
singleton: true,
requiredVersion: deps["react-dom"],
},
},
This will inform Webpack that react and react-dom should be treated as a
singleton, meaning that under no loading circumstances should there ever be more
than one copy of react running at a time. And they also have strict version
requirements.
The sections that follow detail the different hinting options you can send to Webpack’s
SharePlugin through ModuleFederationPlugin, or directly, if you prefer that
level of control.
"shared": {
"react": {
import: "react"
}
}
This will import the react package from the module reference "react".
shareKey
This is the name that will be used to identify the package within the scope. Here is an
example:
"shared": {
"react": {
shareKey: "react"
}
}
This will share the package the "default" scope under the key "react".
"shared": {
"react": {
shareScope: "default"
}
}
This will share the package the "default" scope under the key "react".
singleton
Toggles singleton status for this shared package. If singleton is true, then only
one instance of this package should be running at any time. Here is an example:
"shared": {
"react": {
singleton: true
}
}
The strictVersion flag tells Webpack to use the fallback if there is no version
match. If the singleton flag is enabled and there is no fallback, then Webpack will throw
an error. Here is an example:
"shared": {
"react": {
singleton: true,
strictVersion: true,
version: "^16.12.0"
}
}
This configuration tells Webpack that react is a singleton and it must be in the specified
semantic version of "^16.12.0", and if nothing is available then fallback or throw.
version
The version of the package. Here is an example:
"shared": {
"react": {
version: "^16.12.0"
}
}
This configuration tells Webpack that react should be used at the specified semantic
version.
"shared": {
"react": {
requiredVersion: "^16.12.0"
}
}
This configuration tells Webpack that react must be used at the specified semantic
version.
As you can see, a bunch of thought has gone into making a reliable versioning
mechanism for shared packages in Module Federation.
WHAT’S NEXT
Now that we’ve got a good sense of different ways to load shared packages and run code
from remote modules using Module Federation, let’s look at the exciting architectural
pattern of Fully Federated Applications that is unique to the Module Federation
ecosystem.
Code for this chapter is available on GitHub. This chapter has sub-directories for each of the different
state management libraries. There is also a Blue Collar Coder video associated with this practical use
case.
Ideally we want a shared Header and Footer system with these attributes:
- Live sharing - We want all the micro-sites to update the header at the same time.
Otherwise we get an inconsistent user experience, or worse.
- Sharing without requiring version bumps - The ideal system would not
require the micro-sites to re-deploy with every update.
- Safety and resilience - The header should be absolutely safe and never take down
the applications that consume it.
The great news is that Module Federation allows us to come up with a sharing
architecture that fills all of these requirements.
Home
Nav
HeaderWrapper
FallbackHeader
NPM Build
The frontend code then uses the Module Federation build first, then if an error occurs,
it falls back onto the NPM code which it loads lazily. The Header from Module
Federation is exactly the same as the Header from NPM, which is aliased to
FallbackHeader in the HeaderWrapper.
The only difference is that the Header from Module Federation and the one from NPM
is that the Module Federation version is guaranteed to be up to date with the most
recently released version (i.e. live sharing). Where the NPM version will be whatever
version was imported the last time the Home page was built.
A fun way to think about this approach is like pitons in rock climbing. Module
Federation is the climbing up the rock, but the NPM build is like the piton that keeps
your application from falling too far if your grip slips.
Now that we know what we are building let’s take a look at the example code for this
chapter on Github.
"scripts": {
"build": "npm run build:mf && npm run build:npm",
"build:mf": "webpack --mode production",
"build:npm": "babel src --out-dir build”,
...
},
"main": "./build/index.js",
Now the build creates both a dist directory that has the contents of the Module
Federation build, but also a build directory that has the NPM build. The main key
then points the NPM consumer at the babel transpiled index.js file.
export { Header };
In the video associated with this code we use the background color to differentiate
between an older NPM build of the nav project and the current live Module Federation
code.
Now that we understand the nav project and the Header component we can look to
the consuming home application located in packages/home. The first thing to
understand is that the overall project here, located in the root package.json, is a
workspaces project. So it’s managing the NPM linking between the home and nav
projects.
"dependencies": {
"nav": "1.0.0",
"react": "^16.13.0",
"react-dom": "^16.13.0"
}
To import the Module Federation version of the nav project the webpack.config.js in
the home project has this ModuleFederationPlugin configuration.
new ModuleFederationPlugin({
name: "home",
filename: "remoteEntry.js",
remotes: {
"mf-nav": "nav@http://localhost:8081/remoteEntry.js",
},
exposes: {},
shared: { ... }, // Shared React dependencies
}),
The interesting part here is that we are importing the nav remote but we are aliasing it
to mf-nav within this project. That way in the code we can actively decide whether we
are referencing the NPM version of the Header or the Module Federation version of
the Header.
We lazily import both versions, but for different reasons. The FallbackHeader,
which is sourced from the NPM library, is lazily loaded because we don’t want to bring
in the NPM header unless the Module Federation version fails. The Module Federation
Header is lazily loaded because a failure to do so would bring down the application if
the import failed.
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch() {}
...
}
render() {
if (this.state.hasError) {
return (
<React.Suspense fallback={<div>Loading fallback header</div>}>
<FallbackHeader />
</React.Suspense>
);
}
return (
<React.Suspense fallback={<div>Header loading</div>}>
<Header />
</React.Suspense>
);
}
In the case of an error we lazily render the FallbackHeader, and otherwise we render
the Module Federation Header (which may cause an error and trigger the rendering of
the FallbackHeader).
To try this all out first run yarn and yarn build at the top level of the project. From
there you can run yarn start in each of the packages directories; home and nav. To
check resilience simply stop the yarn start process on the nav project to see the
home application fall back onto the NPM version.
Module Federation also provides additional levels of fallbacks for the Module
Federation code by offering an array of externals. If the first one fails the second will be
tried and so on.
Code for this chapter is available on GitHub. This chapter has sub-directories for each of the different
state management libraries. There is also a Blue Collar Coder video associated with this practical use
case.
There are plenty of places to start on your journey towards implementing Module
Federation across your org, and one of those is using it to implement A/B tests, and a
shared A/B test manager.
A/B tests are a way of seeing true customer preferences through data. You take two
variations on the same piece of interface. In the case of the example code we are
comparing two different image borders:
Variant A has a blue border, and variation B has a green dashed border. In production
this choice would be linked to some meaningful action, for example, clicking an
“Adoption” button to start the adoption process.
- A/B Test Code - The code that renders the different variations
- Test Manager - Some code, or a service, that picks which option the customer will
be presented to the customer
- Results Manager - This is the system that receives the analytics messages
indicating which choice the customer choose, tabulates the results and chooses a
“winner” after a statistically significant number of results have been gathered.
In this example we will be focused on the first two of these, the A/B test code and the
test manager. In reality however, the test and results manager roles are most often
filled by third party systems as services.
The FrameA and FrameB components only vary by the contents of the initial <div>
tag and the styling around the image.
We also load the VariantChooser from the ab-manager remote and invoke it like this:
<VariantChooser
test="test1"
variations={{
a: FrameA,
b: FrameB,
}}
src="https://placedog.net/640/480?id=53"
style={{ width: 640, height: 480 }}
/>
It’s the job of the VariantChooser to roll the dice to see which variation we get, then
to render the correct variation from the variations property. While also sending along
the additional props (i.e. src and style). The test property defines the name of the
test we are running, and what the possible values are. In this case the name is test1
and the possible values are "a" and "b".
Which brings us to the other project in this monorepo and that’s the ab-manager
project which holds the list of available tests, and the VariantChooser implementation.
export default {
test1: ["a", "b"],
};
To get all this working we set up Module Federation this way in the ab-manager:
new ModuleFederationPlugin({
name: "ab_mgr",
filename: "remoteEntry.js",
remotes: {
"ab-manager": "ab_mgr@http://localhost:8080/remoteEntry.js",
},
exposes: {
"./VariantChooser": "./src/VariantChooser",
"./variants": "./src/variants",
},
shared: { ... }, // Shared React dependencies
}),
It’s interesting to notice that the federated modules package is defined as ab_mgr but
we then alias the remote to ab-manager. This is because the name "ab_mgr" is
actually used as Javascript variable and a minus is not valid within a Javascript
variable. So we use the underscore instead.
We also expose the VariantChooser and variants using the exposes key.
new ModuleFederationPlugin({
name: "host",
filename: "remoteEntry.js",
remotes: {
"ab-manager": "ab_mgr@http://localhost:8081/remoteEntry.js",
"host": "host@http://localhost:8080/remoteEntry.js",
},
exposes: {
"./FrameA": "./src/FrameA",
"./FrameB": "./src/FrameB",
},
shared: { ... }, // Shared React dependencies
}),
Specifying host, which is the current project, as a remote also allows us to bring in the
Frame components using standard Module Federation imports:
To run this example run yarn and then yarn start in the root directory. That will launch
both the home and ab-manager applications. You should then have a look at the
network tab to see that the Webpack is only bringing in the required variation.
Code for this chapter is available on GitHub. This chapter has sub-directories for each of the different
state management libraries. There is also a Blue Collar Coder video associated with this practical use
case.
In large enterprise settings it’s no uncommon at all that you will get a division between
the customer facing portion of the site and the administration facing side of the site.
And while that’s maybe fine from an organizational standpoint, it’s not great for code
sharing, particular code that really should be shared. A decent example of that is when
you have the customer facing portion of the site rendering some data that is
administered by the admin team.
When the administration pages want to preview the data as it would appear to the
customer they can either iframe in the production site and patch the data through in a
“preview mode”, or they can just maintain a second copy of the rendering code. But
Module Federation provides a “third way” by making it easy for the customer facing
team to share rendering code with the administration team, or vice versa. And to do
that sharing live so that absolute fidelity is preserved between the customer facing site
and the administration site. A win for everyone!
And the corresponding page on the customer facing site looks like this:
Buddy is such a cute dog! Honestly, if you are looking for good placeholder images you
should check out placedog.net.
The CMS application does not use the EmbedPage and EmbedEditor components
directly. Those are both wrapper components that connect to the REST API endpoint
using CORS so that the requests can go to a different port.
new ModuleFederationPlugin({
name: "cms",
filename: "remoteEntry.js",
remotes: {},
exposes: {
"./EmbedPage": "./src/EmbedPage",
"./EmbedEditor": "./src/EmbedEditor",
},
shared: { ... }, // Shared React dependencies
}),
This EmbedPage component uses the excellent react-query library to connect to the
CMS service defined in the Webpack configuration using an absolute URL.
You could also share out Page from cms-admin for clients specifically interested in
rendering without the data fetching. In the case of this example the EmbedPage makes
it easier on the client with a simple contract; given a page name the component will
make the necessary requests to show the page.
The rest of the cms-admin applications follows this pattern with an Editor
component wrapped by an EmbedEditor that also connects to the service.
This then leads us to the home application that consumes these components.
new ModuleFederationPlugin({
name: "home",
filename: "remoteEntry.js",
remotes: {
cms: "cms@http://localhost:8080/remoteEntry.js",
},
exposes: {},
shared: { ... }, // Shared React dependencies
}),
In the App.jsx for home we then bring in the components using React.lazy imports
and wrap them in local React.Suspense wrappers that provide some nice feedback as
the component loads.
There is another similar wrapper for EmbedEditor and both of these are used in the
main page layout that uses tabs to swap between the static and editable views of the
page content.
An ideal extension to this example would be to use the resiliency pattern as shown in
chapter 9. In this pattern we export the components using both federated modules and
NPM, so that should the live sharing federated modules fail, you have a recent version
as a fallback that you can render.
Code for this chapter is available on GitHub. This chapter has sub-directories for each of the different
state management libraries.
Most large sites run on what’s labeled the “micro site” architecture where a single
domain (e.g. amazon.com) isn’t served by a single application, but instead different
routes will go to different applications. All of these applications work together through
shared session state or shared backend APIs to create what appears to be a single
coherent site for the customer.
The advantage of the micro site architecture is that each of these applications can
deploy independently because the site as a whole doesn’t need to get deployed every
time one route changes. One downside, which we’ve been addressing throughout this
book, is the complexity of sharing code between these different applications. Another
downside is that you lose the ability to create a Single Page Application easily in this
methodology. It also makes it difficult to test an end-to-end flow because you’d have to
inject the experience you are testing into the middle of the entire production end-to-
end flow.
Search
Home Profile
If you're working on the search application, then you federate in the home and profile
applications. As you navigate around the interface, you are still technically running in
the search app, but you get the entire site experience.
This is what we call a Fully Federated Site and it has some big advantages.
- It can be deployed as the site itself, giving your customers a Single Page Application
experience but still allowing the individual teams to deploy independently.
- It provides a better developer experience where the contributor can see how their
feature works through the entire experience.
- It means that you have the ability to completely end-to-end test your entire site even
across multiple projects.
As with our other example applications, if you run yarn start at the top of the chapter 9
directory you will launch all of the servers. The home page runs on port 3001, profile
on 3002, and search on 3003. For simplicity, we’ve put the port numbers in the same
order as the alphabetical order of the project names.
To get a quick taste of the power of a fully federated site, simply make a change to
packages/home/src/Home.jsx and refresh your browser on http://
localhost:3001/. Of course you would expect to see a change there, but when you
navigate to http://localhost:3002/, the profile application, and http://
localhost:3003/, the search application, you will see the changes as well.
The really impressive part about all of this is that, given what we’ve learned thus far
through this book, this is not anything we haven’t already seen. This is just the
culmination of our existing work in understanding Module Federation.
new ModuleFederationPlugin({
name: "home",
filename: "remoteEntry.js",
remotes: {
home: "home@http://localhost:3001/remoteEntry.js",
profile: "profile@http://localhost:3002/remoteEntry.js",
search: "search@http://localhost:3003/remoteEntry.js",
},
exposes: { "./Home": "./src/Home" },
shared: { ... }, // Shared React dependencies
}),
In the React component code for the application, we use the react-router-dom
components to select between the different routes for display:
If you want some more fun, just check out the network tab to see just how little extra
code is loaded as you navigate between the tabs.
This is certainly not the only possible way that you could construct a fully federated
site. You can imagine a very small booter application that only hosts the content frame
and manages navigation between the routes that are supplied via federation. By
default, the application would bring in the routes from production, but by changing a
flag or setting an environment variable an engineer could switch one of the routes to
their own localhost running copy of the part of the interface they are working with.
We have the applications at the top, Home, Search and Checkout, and they connect to
some React components shared using NPM (AddToCart and Frame). And they also
have a shared codebase for the redux store which holds the cart and some shared API
wrappers in the logic NPM module. And all of this sits on top of two micro-services.
And this is the kind of thing we see a lot of nowadays; the "micro-site" architecture. The
most common architecture before that was the "monolith", a single, usually Java
application, where all the frontend code was in one application.
- Code sharing - Code sharing is really difficult - Having shared code in NPM
modules creates friction to sharing code since it has to be extracted from the original
application, which means jumping from repo to repo, doing version bumping, etc.
- Single Page Applications (SPA) - SPAs are difficult to pull off with micro-sites,
but being on a SPA is a huge performance win for the customer. There are great
options like SingleSPA, but those have a learning curve.
- End-to-end testing - E2E testing is very difficult. How do you know when you
release a new version of checkout it's going to be compatible with the other micro-
sites and not break existing flows? That's what end-to-end testing is for, but staging
the entire site using existing tools is very tough.
We are going to step-by-step improve this site using Module Federation until we end
up with a system that runs as a Single Page Application, has easy code sharing, and is
E2E testable. In the accompanying video you can follow along with the step by step
code changes. In this book we will cover the high level architectural moves.
First step is to move the AddToCart React component from components NPM library
to the Checkout application.
This is the natural place for that code because it's functionality that fits within the
purview of the Checkout team. Then we change the Home and Search applications to
remote in the Checkout application and import AddToCart from there.
This means that not only are we reducing the number of NPM libraries we need to
version by one, but we also get live code sharing so that when Checkout updates the
look or the functionality of the “Add To Cart” button the Home and Search sites will
update immediately.
Right now Frame just has anchor tag links to the other applications. In a later step,
because all of the content from all of the applications is exposed as remotes, we will
later Frame to give us full SPA functionality. But for now this is a huge win because the
Home page team can update the Frame header and all the applications will see those
changes immediately after Home page deploys.
All of the applications now use the remote of Checkout to get the store redux code. This
is nothing new, but when we move to a Single Page Application at the end, because the
user never navigates away from the first page we will be able to retain the store in
memory through the entire customer journey.
Checkout manages the Cart Logic because it relates to their role in the system. And the
Search team manages the product related API wrappers which are also shared out to
any other applications that need product data or search capabilities.
Because any site can vend the content from any other site a user can start on the search
page application and never actually leave the original search page as they go perhaps to
Practical Module Federation Page 137 of 166
the home page, then back to search, and then into checkout. And that means your can
use an End-to-End (E2E) testing system to test the entirety of the site from initial
customer visit all the way through ordering an item, with your local code changes run
within that test.
This new system is conceptually easier to understand as there are less moving parts and
shared components and business logic are sharing out of the applications where they
are most ideally located. But, it can be hard to understand how dramatic of a change
full site federation is without trying it yourself. I strongly encourage you to watch the
associated video and to walk through the example before trying to migrate your site to a
fully federated model.
WHAT’S NEXT
It’s not just about sharing modules out of running applications with Module
Federation. There are other ways to share code as well. In the next chapter, we cover
these other methods in detail.
Code for this chapter is available on GitHub. This chapter has sub-directories for each of the different
state management libraries.
Thus far, we’ve been deploying federated modules from applications for consumption
by other applications, but there are alternatives to that model. In this chapter, we will
cover two such models starting with an approach similar to the Micro-Frontend
architecture we covered in the first chapter of this book.
MICRO-FRONTEND MODEL
In the Micro-FE model, we use Webpack to create a dist directory that we then
statically deploy to a static hosting service. Amazon’s S3 service would be a good
example target for this approach.
Applications would then consume the remoteEntry.js just as they would from a
remote application, but in this case there is no running application, just the federated
modules.
Let’s start by looking at the widget directory within the micro-fe directory of the
book code for chapter 10. In that directory, we find the webpack.config.js file that just
packages up the Widget using a standard ModuleFederationPlugin configuration.
...
plugins: [
new ModuleFederationPlugin({
name: "widget",
filename: "remoteEntry.js",
remotes: {},
exposes: {
"./Widget": "./src/Widget",
},
shared: { ... }, // Shared React dependencies
}),
],
};
In the package.json, we have a build script runs the webpack build and a start
script that runs a static server that imitates something like S3.
"scripts": {
"build": "webpack --mode production",
"start": "cd dist && PORT=3002 npx servor"
},
To run this widget server, first run yarn build then yarn start.
To test it out, run yarn start in the home directory that is a peer to the widget
directory.
...
The important thing to get from this exercise is that you don’t necessarily have to have
the federated module code deployed with the application. This leads us to a second
possible build and deployment option--the sidecar.
This project is, as you might guess from the name, a React application that is packaged
using Webpack 4. However, we want to expose the src/Carousel.jsx file as a
federated module. How do we do that? We create a Webpack 5 project within this
project in a directory called wp5-sidecar (though you can call it whatever you want.)
module.exports = {
module: {
rules: [
... // Rules for CSS and pjs
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-react"],
},
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: "widget",
filename: "remoteEntry.js",
remotes: {},
exposes: {
"./Carousel": "../src/Carousel",
},
shared: { ... }, // Shared React dependencies
}),
],
};
The first thing to notice is that we are setting up the babel options in the Webpack
configuration because babel will not find them with the relative pathing that we use to
get to the Carousel module.
It’s true that the Webpack 4 code won’t know about the federated module Javascript or
use it. However, this is a method for exporting code from Webpack 4 applications today
if there is going to be a lengthy migration process to Webpack 5.
With Module Federation making it very easy to share code and components between
applications it became readily apparent that we needed a way to visualize everything
being shared between applications. So we created the Module Federation Dashboard
and it’s available on Docker today. Here are some screenshots from a populated
dashboard.
This will create a local directory named dashboard-data that will hold the data files
so that there is no data loss when the Docker image is shutdown.
Once this is running you can go to http://localhost:3000/ to see the dashboard. When
no data is loaded you will get a home page that explains the dashboard, what it’s useful
for and how to integrate with it.
To send data to the Dashboard we’ve developed a plugin that integrates right into your
Webpack configuration. To get started first install the plugin in your project:
There is also additional metadata that you can add to the DashboardPlugin
configuration that is documented on the NPM page for the plugin.
We are really excited about the potential for the Dashboard. It’s a level of detail and
insight that isn’t available from NPM. As we continue to add new features to enhance
the experience and stabilize the application
It’s hard to cover every potential use case for a technology as fundamental as Module
Foundation. In this chapter, we cover a variety of related topics and questions that
often arise as we roll this out in projects and organizations.
When you have one of the examples in this book up and running it’s worth having a
look at the remoteEntry.js file to see what is in it so that you can get a better
understanding how Module Federation works at the code level. By default the URL for
remoteEntry.js is http://localhost:8080/remoteEntry.js in the starter kit project.
You can name the remoteEntry.js file whatever you choose. The one truly
important detail is that the publicPath defined in the webpack.config.js
matches where the code is deployed. Because the remoteEntry.js contains
references to the code, and not the code itself, Webpack uses the publicPath to find
the modules and load them asynchronously. So that publicPath must match the
deployment location for the system to work.
Practical Module Federation Page 148 of 166
Can I use Module Federation with SingleSPA?
Absolutely! In fact Module Federation is one of the two code loading styles that is
covered on the SingleSPA documentation website. The advantage of using SingleSPA in
tandem with Module Federation is that SingleSPA extends the capabilities of the
solution by allowing you to host view code from multiple view frameworks (e.g. React
can run on a Vue page, or vice versa.) And Module Federation allows you to share other
types of code as well (i.e. business logic, etc.)
A/B test code is an ideal use for Module Federation. You can expose both of the
variants and use asynchronous imports (or React.lazy) to import which ever variant
your A/B provider tells you to. In this solution you will ever only import one variation
of the code.
This question is really about the architecture of your solution and as such you should
create your own criteria for that decision. That being said, here is some high level
guidance on when to choose NPM over Module Federation.
- Externally shared code - If you are writing a wrapper for your API then
developers will expect to see that on NPM.
- Slow moving business logic - If it’s not going to change that often, and the
upgrade procedures are very specific, then it’s probably best controlled and
versioned as an NPM module.
Module Federation is a new architectural paradigm and it will take time for the
industry to adapt. It’s worth spending the time before rolling out Module Federation at
scale to develop guidance on how to decide on packaging code as NPM packages or as
federated modules.
It’s worth noting that any code shared with NPM can always be imported an either
shared using the shared module system or exports to other applications using exposes.
We see some common errors in Module Federation. So here’s what’s going on if you see
one of them.
You can set the dependency as eager inside the advanced API of Module Federation,
which doesn’t put the modules in an async chunk, but provides them synchronously.
This allows us to use these shared modules in the initial chunk. But be careful as all
provided and fallback modules will always be downloaded.
shared: {
...deps,
react: {
eager: true,
singleton: true,
requiredVersion: deps.react,
},
"react-dom": {
eager: true,
singleton: true,
requiredVersion: deps["react-dom"],
},
module: {
rules: [
{
test: /bootstrap\.js$/,
loader: "bundle-loader",
options: {
lazy: true,
},
},
]
}
//index.js
import bootstrap from "./bootstrap";
bootstrap();
Create a bootstrap.js file, and move the contents of your entry point code into that
file
//bootstrap.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
Methods mentioned above work, but can have some limits or drawbacks.
import('./bootstrap')
exposes: {
"Button": "./src/Button",
},
To this:
exposes: {
"./Button": "./src/Button",
},
The change is done so that we can follow the ESM syntax inside Node 14
This is a reference for all the options associated with the four different plugins that
make up the Module Federation system.
ModuleFederationPlugin
Name
This is the name of the module being exported.
Library (optional)
This specifies how the module is to be formatted for use in the browser context.
You should use the assign type and match the name with the module name.
Examples
This sets the Webpack library target type to var under the name app1. There are
many library target types, we recommend var as it will be assigned to a global
variable exposed under that name and be easily referenced.
The file name for the remote entry. The recommended value for this key is
“remoteEntry.js”. This key is only required if the application is exposing
modules.
Remotes (optional)
An object that specifies the remotes this application consumes. Remotes is only
required if you are going to be consuming remote modules.
Examples
{ remotes: {
nav: "nav@http://localhost:3001/remoteEntry.js",
} }
This specifies that the incoming nav federated module is a known remote and
that it can be imported using the name nav. And that the remote entry is at the
specified URL.
This specifies that the incoming nav federated module is a known remote and
that it can be imported using the name checkoutNav.
remoteType (optional)
Examples
{ remotes: {
nav: "nav",
}, remoteType: “var" }
exposes: {
"./Header": "./src/Header",
},
This exposes the source file ./src/Header as the named export Header.
exposes: {
Header: {
import: “./src/Header”,
// optional metadata
}
},
This exposes the source file ./src/Header as the named export Header with
the additional metadata provided in the object.
Examples
shared: {
"react": "^16.12.0"
},
This shares react with the NPM semver mechanics starting at 16.12.
{ shared: require(“./package.json").dependencies },
This shares react but marks it as a singleton meaning that only one copy can be in
use in the application at one time.
{shared: {
react: {
version: "^16.12.0",
import: "react",
shareKey: "react",
shareScope: "default",
singleton: "true",
}
},
This shares react at 16.12 as a singleton with the import name as react and in the
default scope under the name react.
Examples
This sets the name of the scope to use for the shared packages to “default”.
ContainerPlugin
This is the plugin that exposes modules for Module Federation. It accepts the name,
library, filename, and exposes keywords with the same options as
ModuleFederationPlugin.
ContainerReferencePlugin
This is the plugin manages remotes for Module Federation. It accepts the remoteType
and remotes keywords with the same options as ModuleFederationPlugin.
SharePlugin
This is the plugin manages shares for Module Federation. It accepts the shares and
sharesScope keywords with the same options as ModuleFederationPlugin.
Below is a glossary of the different terms used in this book and what they mean.
Term Description
Exposed A module that is exported from an application is “exposed”
Host The currently running application that is hosting federated
remote modules
Module The smallest unit of shareable code. In the Webpack system, a
module can be a single file.
Override/Shared The Webpack internal name for a shared package
Package An NPM package
Remote A reference to an external federated module
Remote Module Any file exported through Module Federation
Scope A scope is synonymous with an application. Every application
is exported as one named scope.
Shared A module that is shared from an application to a remote
module.
Singleton A constraint that only a single version of this module or
package should be in use within that application at any given
time