One of the first articles we covered in tech book club was Micro Frontends, an approach to scaling frontend development across many independent and autonomous teams.
Although the content of the article is well-articulated, the accompanying example is lacking. It hacks create-react-app with an extra package to enable Webpack builds and offers no mechanism to run all of the micro frontend applications in tandem. The example is easy to follow, but inspires no confidence for a real-world scenario.
After experimenting with different tools and approaches, I think I've constructed a better scaffold for micro frontends that improves the overall developer experience. This article walks you through that approach.
You can find the complete example here.
Monorepos with Nx
One of the major disadvantages to micro frontends is complexity. Rather than maintaining all of your application code in one place, that code is now spread across multiple applications and managed by separate teams. This can make collaboration on shared assets difficult and tedious.
Keeping each micro frontend within the same repository (monorepo) is one easy way to help manage this complexity. Google famously uses this technique to manage its billion-line codebase, relying on automation and tools to manage the trade-offs.
Rather than using create-react-app to bootstrap micro frontends, turn instead to Nx. Nx is a build framework that offers tools to manage a multi-application monorepo, a perfect fit for micro frontends.
Here are a few ways Nx helps manage micro frontends:
- Script orchestration: run servers/builds for multiple micro frontends concurrently with a single command.
- Share common components and code libraries conveniently without introducing lots of Webpack overhead.
- Manage consistent dependency versions.
- Run builds and tests for affected changes across micro frontends based on dependency graphs.
Nx is certainly not the only tool that supports monorepos, but I’ve found it to be a great fit for micro frontends thanks to its built-in React support and batteries-included functionality. Lerna is noteworthy alternative that offers less built-in functionality at the advantage of flexibility.
Detailed example
Nx requires only a few configuration changes to support micro frontends and you won’t need the help of an ejection tool like react-app-rewired
.
- Create a new Nx workspace with two React applications (one container, one micro frontend).
- Extend Nx’s default React Webpack configuration to disable chunking and generate an asset manifest.
- Implement conventional micro frontend components as described in the Thoughtworks article.
- Tie it all together with a single
npm start
script.
1. Create the Nx workspace
Begin by creating a new Nx workspace:
npx create-nx-workspace@latest micronx
? What to create in the new workspace...
> empty
Use Nx Cloud?
> No
Navigate into the new micronx
directory and create two React applications, one container and one micro frontend. It’s important to select styled-components
(or another CSS-in-JS solution) so that your component CSS is included in the micro frontend’s JS bundle.
cd ./micronx
npm install --also=dev @nrwl/react
# Container application
nx g @nrwl/react:app container
> styled-components
> No
# Micro frontend
nx g @nrwl/react:app dashboard
> No
So far you've created a monorepo with two separate React applications: container and dashboard. Either React application can be served independently via its respective nx run <app>:serve
script, but there's nothing yet in place to have them work together.
The next step sprinkles in some configuration changes that allow you to dynamically load the dashboard application as a micro frontend.
2. Modify micro frontend Webpack configuration
Nx stores most of its relevant configuration in the workspace.json
file stored at the project's root.
You need to modify workspace.json
to point the micro frontend’s Webpack configuration to a new file, webpack.config.js
. This new file contains the configuration updates necessary to support dynamically loading the micro frontend.
Note that you don’t need to do this for the container, since the container is not a micro frontend.
// workspace.json
"projects": {
"dashboard": {
"targets": {
"build": {
// ...
"webpackConfig": "webpack.config.js"
}
}
}
}
Now you need to create that file, webpack.config.js
, at the root directory of the project.
This modified Webpack configuration extends the default code from @nrwl/react to avoid losing any functionality. Following the Thoughtworks example, two modifications are needed to support conventional micro frontends:
- Disable chunking so the container application loads one bundle per micro frontend.
- Add
WebpackManifestPlugin
to map the generated JS output to an easy import path (taken from react-scripts webpack configuration).
npm install --also=dev webpack-manifest-plugin
// webpack.config.js
const reactWebpackConfig = require('@nrwl/react/plugins/webpack')
const { WebpackManifestPlugin } = require('webpack-manifest-plugin')
function getWebpackConfig(config) {
config = reactWebpackConfig(config)
// Disable chunking
config.optimization = {
...config.optimization,
runtimeChunk: false,
splitChunks: {
chunks(chunk) {
return false
},
},
}
// Enable asset-manifest
config.plugins.push(
new WebpackManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: '/',
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path
return manifest
}, seed)
const entrypointFiles = entrypoints.main.filter(
fileName => !fileName.endsWith('.map'),
)
return {
files: manifestFiles,
entrypoints: entrypointFiles,
}
},
}),
)
return config
}
module.exports = getWebpackConfig
Run nx run dashboard:serve
and visit http://localhost:4200/asset-manifest.json. Note that the dashboard application now only has one entry point: main.js
.
{
"files": {
"main.js": "/main.js",
"main.js.map": "/main.js.map",
"polyfills.js": "/polyfills.js",
"polyfills.js.map": "/polyfills.js.map",
"assets/.gitkeep": "/assets/.gitkeep",
"favicon.ico": "/favicon.ico",
"index.html": "/index.html"
},
"entrypoints": ["main.js"]
}
3. Add in micro frontend components
With Nx configured properly, the next step is to follow Thoughtworks example and introduce all of the micro frontend functionality.
The following links don't deviate from the article, but are included for completeness.
Use the
MicroFrontend
component to load the dashboard micro frontend in the container.Export render functions so the dashboard micro frontend no longer renders itself to the DOM.
Update the dashboard's
index.html
so it can still be served independently.
4. Tie everything together
The last step is to serve the micro frontend and container together. Add concurrently
and modify your start script to serve the dashboard on a specific port.
"start": "concurrently \"nx run container:serve\" \"nx run dashboard:serve --port=3001\""
Run npm start
and you've got micro frontends.
Working with Nx
Serving micro frontends
Nx doesn't have out-of-the-box functionality for serving multiple applications simultaneously, which is why I resorted to concurrently
in the above example. That said, running individual micro frontends is made easy with the Nx CLI.
- Develop micro frontends independently via
nx run <project>:serve
. - See how they fit into the whole application via
npm start
.
Generators
Nx ships with a handful of generators that help scaffold your application. In particular, the library generator makes it really easy to share React components:
nx g lib common
This creates a new React library in your project's libs/
directory with a bunch of pre-configured build settings. Included is a convenient TypeScript path alias that makes importing the library straightforward:
// apps/dashboard/src/app/app.tsx
import { ComponentA, ComponentB } from '@micronx/common'
Nx provides additional benefits to sharing code this way by keeping track of your project's dependency graph. The relationships between your various code libraries and each dependent application can be illustrated by running nx dep-graph
.
Internally, Nx uses this dependency graph to reduce the number of builds/tests that need to be run when changes are introduced. If you make a change to apps/dashboard/
and run nx affected:test
, Nx will only run tests for the Dashboard micro frontend. This becomes very powerful as the dependency graph of your project grows in complexity.
Optimizations
Something unique to the micro frontend strategy is the duplication of common vendor dependencies and shared code libraries in the production JS bundles.
The Thoughwork's article touches on this in the "Common Content" section, advocating for tagging common dependencies as Webpack externals to prevent them from being included in each application's final bundle.
module.exports = (config, env) => {
config.externals = {
react: 'React',
'react-dom': 'ReactDOM',
}
return config
}
Once Nx upgrades its React tools to Webpack 5, a new method of code optimization will be available for micro frontend projects via Module Federation. This strategy enables building shared code libraries (libs/
) into the container application, removing yet another common dependency from the micro frontend bundles.
Top comments (4)
wow this tool for managing MFEs look really useful! thank you for introducing it
🔥🔥🔥🔥
Why disable chunking?
The container application has to fetch a micro frontend's JS dynamically and load it into the page. This wouldn't be possible (or at least, it would require quite a bit of fancy engineering) if the micro frontend were chunked, since the container would need to fetch all of the separate JS files, reconcile load order, and figure out which chunks are actually needed.
The Thoughtworks article has a bit more detail on this subject in their example.