DEV Community

loading...

Better React Micro Frontends w/ Nx

Graham Marlow
I'm Graham Marlow, a software engineer who's spent too much time on the web.
・6 min read

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.

  1. Create a new Nx workspace with two React applications (one container, one micro frontend).
  2. Extend Nx’s default React Webpack configuration to disable chunking and generate an asset manifest.
  3. Implement conventional micro frontend components as described in the Thoughtworks article.
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Disable chunking so the container application loads one bundle per micro frontend.
  2. 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
Enter fullscreen mode Exit fullscreen mode
// 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
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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.

  1. Create a new component, MicroFrontend, in the container.

  2. Use the MicroFrontend component to load the dashboard micro frontend in the container.

  3. Export render functions so the dashboard micro frontend no longer renders itself to the DOM.

  4. 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\""
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (4)

Collapse
lyqht profile image
Estee Tey

wow this tool for managing MFEs look really useful! thank you for introducing it

Collapse
moniruzzamansaikat profile image
Moniruzzaman Saikat

🔥🔥🔥🔥

Collapse
clamstew profile image
Clay Stewart

Why disable chunking?

Collapse
mgmarlow profile image
Graham Marlow Author

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.