That headline is a mouth-full, I know!
In the past several years I have been working on distributed and multiple teams as well as being a pretty early adopter of NextJS (since around V2.0!) in production. I've worked on micro frontends with shared npm
packages while trying to orchestrate one cohesive user experience.
It was and is hard.
That’s why I have been closely following the latest developments in the field, and since I’ve heard about Webpack 5 Module Federation, I was curious how and when it would work with an amazing framework such as NextJS.
I guess the title and all those buzzwords need a little breakdown and explaining before we get down to business, so… here we go!
What are Micro Front Ends?
Micro Front Ends are like microservices for the front end. Think about it as an encapsulated, self-contained piece of code or component that can be consumed anywhere. To quote micro-frontends.org:
"The idea behind Micro Frontends is to think about a website or web app as a composition of features which are owned by independent teams. Each team has a distinct area of business or mission it cares about and specializes in. A team is cross functional and develops its features end-to-end, from database to user interface."
Source: https://micro-frontends.org/
You can read more about this concept in the provided link above or here. The key core concepts to remember:
- Technology agnostic
- Isolated Team Code
- Build a resilient site / App
There are several frameworks and approaches to implement this architecture, but this is not the subject of this post. I will be focusing on sharing code.
What's Module Federation?
Technically speaking, Module Federation is a Webpack v5 feature which allows separate (Webpack) builds to form a single application. However, it's much more than that...
To paraphrase Zack Jackson (don't remember where I heard it or saw it), one of the creators of Module Federation:
It is a distributed application architecture. Independently deployed bundles working as a monolith at runtime.
So, in a few bullet points:
- It’s a type of JavaScript architecture.
- It allows a JavaScript application to dynamically load code from another application
- It allows haring dependencies - if an application consuming a federated module does not have a dependency needed by the federated code — Webpack will download the missing dependency from that federated build origin.
- Orchestrated at runtime not build time - no need for servers - universal
Module Federation is a tool based approach to implementing micro front-end architecture.
It is important not to confuse Module Federation
with Webpack [DllPlugin](https://webpack.js.org/plugins/dll-plugin/)
which is a tool mostly focused on improving build time performance. It can be used to build apps that depend on DLLs (Dynamic Link Library), but this can cause deploy delays, there is the extra infrastructure for compile-time dependency, it needs to rebuild when parts change (which causes deploy delays), and it is highly dependent on external code with no fail-safe. In summary, DLLs don't scale with multiple applications and require a lot of manual work for sharing.
Module Federation, on the other hand, is highly flexible while allowing only less deploy delay due to needing only the shared code and app to be built. It is similar to Apollo GraphQL federation but applied to JavaScript modules - browser and Node.js
.
Some terminology that is useful to know when talking about Module Federation:
- Host: A Webpack build that is initialized first during a page load
- Remote: Another Webpack build, where part of it is being consumed by a “host”
- Bidirectional-hosts: can consume and be consumed
- Omnidirectional-hosts: A host that behaves like a remote & host at the same time
I could blabber on a lot more about this, but if you want to learn more you can visit the official website, you can get the "Practical Module Federation" book, or you can check out the resources section.
What's NextJS?
If you're not familiar with the frontend/React ecosystem or you have been living under a rock, NextJS is a React framework for building hybrid static and server-side rendered React application.
Basically, it takes off a lot of the hassle of configuring, tinkering, and retrofitting what it takes to get a React application (or website) to Production.
It has a large variety of features out of the box that just makes any web developer grin like a giddy school girl.
To name a few key features:
- Zero configuration
- TypeScript Support
- File-system routing
- Built-in serverless functions (AKA API routes)
- Code splitting and bundling
You can also hear a little bit about what NextJS is in a talk I gave (my first one! pardon my nervousness there 😅) - keep in mind this was a year and a half ago from the time of this writing and the NextJS team have added a whole lot of features and optimizations since.
For the sake of this post, it is important to remember that frameworks have limitations and in this tutorial, we are fighting some of the limitations NextJS has. The team behind NextJS has made incredible strides in a short period of time. However, to be able to use Module Federation we will need to work around some key aspects, such as no Webpack v5 support (yet) and the framework is not fully async.
What are we going to build?
We're going to build 2 Next JS apps:
- Remote App (App 1)- will expose a React component and 2 functions
- Consumer (App 2) - will consume code/components from the first app.
You can also watch this great video by Jack Herr if you're more into that.
If you want to skip all of this and see all of the code, here's a link to the repo.
So.. after that's out of our way...
Let's do it!
First Steps:
- Create a folder to hold both apps.
- To kick start the first app go into the created folder and run :
npx create-next-app app1
- Kick start the second (notice that this time its
app2
):
npx create-next-app app2
Ok, now we should have 2 apps with NextJS with a version that should be ^9.5.6
.
If you want to stop and try to run them to see they work, just go to their folders and start them off with:
yarn run dev
Now, in order to use Module Federation, we need Webpack v5, but alas, at the time of this writing Next's latest version still runs Webpack 4. 😢
But don't panic yet! Luckily for us, our friend Zack has us covered with a little nifty package for this transition period called @module-federation/nextjs-mf
!
Setting up our remote app:
Step 1
Go into app1
and run:
yarn add @module-federation/nextjs-mf
Step 2
In order to use Webpack 5 with our Next apps we're going to need to add resolutions to our package.json
:
"resolutions": {
"webpack": "5.1.3"
},
What this does is tell our package manager to use this specific version of Webpack we want to use. But because we've used create-next-app
to bootstrap our app, we now need to clean up our node_modules
:
// in the same folder for app1 delete node_modules:
rm -rf node_modules
// re-install all of our pacakges, but this time Webpack 5 should be installed:
yarn install
Our boilerplate code is almost ready. What we are missing at this point are the modules we would want to expose to our consumer app.
Let's add some.
Step 3
First we'll create just a simple Nav
component:
import * as React from 'react';
const Nav = () => {
return (
<nav
style={{
background: 'cadetblue',
width: '100%',
height: '100px',
color: 'white',
textAlign: 'center',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '24px',
}}>
Nav
</nav>
);
};
export default Nav;
Now just to make sure it's working we'll add it to our index.js
page and see it render:
import Nav from '../components/nav'
export default function Home() {
return (
<div className={styles.container}>
{/* JSX created by create-next-app */}
<main className={styles.main}>
<Nav />
{/* mroe JSX created by create-next-app */}
</main>
</div>
)
}
If we run yarn dev
in app1
folder and go to localhost:3000
we should see something like this:
Step 4
We'll add two functions to export as well:
// utils/add.js
const add = (x,y) => {
return x + y;
}
export default add
// utils/multiplyByTwo.js
function multiplyByTwo(x) {
return x * 2;
}
export default multiplyByTwo
Step 5
After these steps we should be able to use configure our Module Federation Webpack plugin. So, we need to create a next.config.js
file in the root folder and add this:
const {
withModuleFederation,
MergeRuntime,
} = require('@module-federation/nextjs-mf');
const path = require('path');
module.exports = {
webpack: (config, options) => {
const { buildId, dev, isServer, defaultLoaders, webpack } = options;
const mfConf = {
name: 'app1',
library: { type: config.output.libraryTarget, name: 'app1' },
filename: 'static/runtime/remoteEntry.js',
// This is where we configure the remotes we want to consume.
// We will be using this in App 2.
remotes: {},
// as the name suggests, this is what we are going to expose
exposes: {
'./nav': './components/nav',
'./add': './utils/add',
'./multiplyByTwo': './utils/multiplyByTwo',
},
// over here we can put a list of modules we would like to share
shared: [],
};
// Configures ModuleFederation and other Webpack properties
withModuleFederation(config, options, mfConf);
config.plugins.push(new MergeRuntime());
if (!isServer) {
config.output.publicPath = 'http://localhost:3000/_next/';
}
return config;
},
};
Step 6
Next, we need to add pages/_document.js
:
import Document, { Html, Head, Main, NextScript } from "next/document";
import { patchSharing } from "@module-federation/nextjs-mf";
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}
render() {
return (
<Html>
{/* This is what allows sharing to happen */}
{patchSharing()}
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
Side note :
for easing this process it is possible to install @module-federation/nextjs-mf
globally (yarn global add @module-federation/nextjs-mf
) and from app2
folder run:
nextjs-mf upgrade -p 3001
This will setup up your package.json
, _document.js
, and next.config.js
from the exposing app set up steps (2, 5, 6) as well as set up the running script for this app to run on PORT:3001
to avoid port clashes.
However, the caveat of this method (at the time of this writing) is that for some reason this changes our NextJS version and nexjs-mf
package version to older ones (in package.json
):
{
"name": "app2",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^9.5.6-canary.0",
"react": "17.0.1",
"react-dom": "17.0.1",
"@module-federation/nextjs-mf": "0.0.1-beta.4"
},
"resolutions": {
"webpack": "5.1.3",
"next": "9.5.5"
}
}
Just be aware if you use this method.
Setting up our consumer app:
If you've opted out of using the above method, make sure you're package.json
looks like this:
{
"name": "app2",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "10.0.2",
"react": "17.0.1",
"react-dom": "17.0.1",
"@module-federation/nextjs-mf": "0.0.2"
},
"resolutions": {
"webpack": "5.1.3"
}
}
Then we need to repeat the same steps as in Step1 and Step2 from the exposing app (add resolutions, remove node_modules
and reinstall), just make sure you're targeting app2
folder.
Next, create your next.config.js
:
const {
withModuleFederation,
MergeRuntime,
} = require('@module-federation/nextjs-mf');
const path = require('path');
module.exports = {
webpack: (config, options) => {
const { buildId, dev, isServer, defaultLoaders, webpack } = options;
const mfConf = {
name: 'app2',
library: { type: config.output.libraryTarget, name: 'app2' },
filename: 'static/runtime/remoteEntry.js',
// this is where we define what and where we're going to consume our modules.
// note that this is only for local development and is relative to where the remote
// app is in you folder structure.
remotes: {
// this defines our remote app name space, so we will be able to
// import from 'app1'
app1: isServer
? path.resolve(
__dirname,
'../app1/.next/server/static/runtime/remoteEntry.js'
)
: 'app1', // for client, treat it as a global
},
exposes: {},
shared: [],
};
// Configures ModuleFederation and other Webpack properties
withModuleFederation(config, options, mfConf);
config.plugins.push(new MergeRuntime());
if (!isServer) {
config.output.publicPath = 'http://localhost:3001/_next/';
}
return config;
},
};
Then add _document.js
:
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { patchSharing } from '@module-federation/nextjs-mf';
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}
render() {
return (
<Html>
{patchSharing()}
{/* This is where we're actually allowing app 2 to get the code from app1 */}
<script src="http://localhost:3000/_next/static/remoteEntryMerged.js" />
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
Now we can start consuming modules from app1! 🎉🎉🎉
Let's import those modules in our pages/index.js
:
// We need to use top level await on these modules as they are async.
// This is actually what let's module federation work with NextJS
const Nav = (await import('app1/nav')).default;
const add = (await import('app1/add')).default;
const multiplyByTwo = (await import('app1/multiplyByTwo')).default;
export default function Home() {
return (
<div className={styles.container}>
{/* JSX created by create-next-app */}
<main className={styles.main}>
<Nav />
<h2>
{`Adding 2 and 3 ==>`} {add(2, 3)}
</h2>
<h2>
{`Multiplying 5 by 2 ==>`} {multiplyByTwo(5)}
</h2>
{/* mroe JSX created by create-next-app */}
</main>
</div>
)
}
Let's check that everything works as expected:
// run in /app1 folder, and then in /app2 floder:
yarn dev
Go to your browser and open [localhost:3001](http://localhost:3001)
(app2) and this is what you should see:
We were able to consume a component and 2 modules from app1
inside of app2
! 🚀🚀🚀
This is where some more magic comes in:
- Go to
app1/nav
and change thebackgroundColor
property to something else likehotpink
and hit save. - Stop
app2
server and rerun it withyarn dev
again
If you refresh [localhost:3001](http://localhost:3001)
you should see this result:
What happened here? We were able to simulate a code change in app1
that was received in app2
without making any changes to the actual code of app2
!
Issues and Caveats Along The Way
When I first started off playing around with this setup I ran into an issue where I got a blank screen on the consumer app, apparently, it was due to the naming of my apps and folders. I've even opened up an issue about this in the next-mf
package. In short, Don't use kebab case names and pay attention to the file paths 🤷🏽 🤦🏾.
Another important note is that exposing components and pages as modules works well, but there are issues when you try to use NextJS Link
component.
Lastly, note that you cannot expose _app.js
as a shared module.
Deployment
I thought it would be cool to see this project running in a production environment, so I went on and tried to deploy the two apps to 2 popular cloud hosting services:
Vercel - ****Attempted to deploy there, didn't work due to Webpack 5 resolutions and a clash in the platform. I have opened a ticket in their support system but still have yet to resolve the issue.
Netlify - As it is, Netlify only support sites to be deployed with the JAMStack architecture, so it only supports NextJS with static HTML export. When running a build locally, I was able to get both apps working while sharing modules even when using next export
- the important file remoteEntryMerged.js
was created in the .next
build folder:
However after deploying with the correct environment variables in place, for some reason that file is missing from the sources:
Hopefully, I will be able to sort one of these out at some point. When and if I do, I will update. But as it seems, in order to get this sort of stack running in an actual production environment there is some tinkering to do. I do believe that if you try to just copy the build folder as it outputted locally to an S3 bucket or something similar, it should probably work.
Conclusion
In this post, we've seen how to set up and work with Module Federation and NextJS which allows us to share code and components, which in a way, is what allows micro frontends.
This is probably only a temporary solution to get this set up working until NextJS upgrades to Webpack 5.
One thing to keep in mind with Module Federation and using this type of architecture is that it comes with a slew of challenges as well. How to manage versions of federated modules is still in it's early days, only a handful of people have actually used it in production. There is a solution being worked on by Zack Jackson (and I'm helping out! 😎) called Federation Dashboard which uses "Module Federation Dashboard Plugin", but it's still in the making...
Another challenge might be shared modules sharing breaking contracts or APIs with consuming apps.
Then again, these are solvable problems, just ones that haven't been iterated enough through yet.
I am a strong believer in the technologies and architecture I've touched on in this post and I'm excited to see what the future holds!
Resources
Module Federation for NextJS 10
Module Federation in Webpack 5 - Tobias Koppers
Webpack 5 Module Federation - Zack Jackson - CityJS Conf 2020
Top comments (5)
Hi Yoav, thanks for this article. I noticed that you used the
style
attribute in your remoteNav
component. Did you try using CSS Modules to style theNav
component, and how do you retrieve thelink
orstyle
tags to insert into the SSR-ed HTML markup?Next.js docs on CSS Modules support
Hi Shun,
Delighted you liked the article :-)
I have actually not tried using CSS modules in this example, but I believe it would be possible to do so. I might be mistaken, but I think that if you do use them in a remote component the CSS would be bundled with the federated component.
Well... went on and tried just so I can see that I was right, and indeed it does work :-)
There's no need to retrieve any
link
orstyle
tag - the federated code has it all.You can check out this branch that shows it working.
Hope this helps!
Thanks for your prompt response!
Hi there! Thanks for the article, I've been trying to achieve this on NextJS 10+ and this tutorial is not working anymore, all the articles I could find are exactly the same, also in all of them are using NextJS version 9+, any idea or solution to get this working with version 10?
I think one of the issues it's that:
@module-federation/nextjs-mf
now it's been updated and it's not a free plug-in anymore, you can see for example functionwithModuleFederation
doesn't exist anymore and now its calledwithFederatedSidecar
Maybe using a previous version with support will work