Why Use Single-SPA and Nx for Microfrontends?
Single-SPA and Nx each bring unique strengths to the microfrontend architecture:
- Single-SPA: This framework enables multiple frontend applications (microfrontends) to coexist on the same page, handling application lifecycles, routing, and communication between microfrontends. Single-SPA allows different frameworks (such as React, Angular, and Vue) to coexist.
- Nx: Nx is a powerful monorepo management tool with built-in support for modular development, dependency management, and build optimization. Nx provides a streamlined way to manage multiple applications and libraries in a single repository.
Combining these tools allows organizations to scale frontend development, increase team productivity, and support a variety of frontend technologies within a single monorepo.
Setting Up a Monorepo with Nx
To get started, we’ll first set up an Nx workspace for our monorepo. The workspace will contain both the root configuration (which handles global settings and routing) and individual microfrontends.
Step 1: Install Nx
If Nx isn’t already installed, you can add it globally with the following command:
npm install -g nx
Step 2: Initialize an Nx Workspace
Create a new Nx workspace by running:
npx create-nx-workspace@latest microfrontend-monorepo
During setup, choose an empty workspace (with no preset) to give us complete control over the structure. Once created, your workspace will look like this:
/apps
/libs
/tools
Setting Up Single-SPA in the Nx Monorepo
With the Nx workspace set up, we can now add Single-SPA and configure it for microfrontend integration.
Step 3: Install Single-SPA Dependencies
To enable Single-SPA, install the necessary packages:
npm install single-spa single-spa-layout
These packages provide the base framework for handling microfrontends and layouts within Single-SPA. You’ll also need a bundler like Webpack, which we’ll configure for each microfrontend.
Step 4: Create the Root Config
The root config handles routing between microfrontends and controls which applications are loaded based on the URL. To create the root config, generate a new application in the apps
directory:
nx generate @nrwl/react:app root-config
In this directory, create an index.ejs
file to serve as the main HTML template:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Microfrontend App</title>
</head>
<body>
<div id="single-spa-root"></div>
<script src="/apps/root-config/main.js"></script>
</body>
</html>
Next, create a single-spa-config.js
file in root-config
to manage routing and lifecycle events for the microfrontends:
import { registerApplication, start } from 'single-spa';
registerApplication({
name: '@project/app1',
app: () => System.import('@project/app1'),
activeWhen: ['/app1']
});
registerApplication({
name: '@project/app2',
app: () => System.import('@project/app2'),
activeWhen: ['/app2']
});
start();
This configuration registers app1
and app2
to load when users navigate to /app1
and /app2
, respectively.
Creating Microfrontends in Nx
Now that the root config is set up, we can create individual microfrontend applications within the Nx monorepo. Each microfrontend will be a standalone application registered with Single-SPA.
Step 5: Generate Microfrontend Applications
Create two microfrontend applications in the apps
directory:
nx generate @nrwl/react:app app1
nx generate @nrwl/react:app app2
These commands generate basic Nx applications. Each app has its own main.js
file, which serves as the entry point.
Step 6: Configure Single-SPA Lifecycle in Each Microfrontend
In each microfrontend’s main.js
, modify the code to use Single-SPA’s lifecycle methods:
import { registerApplication, start } from 'single-spa';
function mount() {
ReactDOM.render(<App />, document.getElementById('root'));
}
function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}
export const bootstrap = [() => Promise.resolve()];
export const mount = [mount];
export const unmount = [unmount];
start();
The mount
and unmount
functions control rendering of the microfrontend, while Single-SPA handles loading and unloading based on the root config.
Setting Up Webpack for Microfrontends
Each microfrontend needs to be bundled in a way that allows it to be dynamically loaded by Single-SPA. Webpack is commonly used for this purpose.
Step 7: Configure Webpack for Each Microfrontend
Create or update the webpack.config.js
file in each microfrontend’s directory:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { merge } = require('webpack-merge');
const path = require('path');
module.exports = merge(commonConfig, {
output: {
filename: '[name].js',
libraryTarget: 'system'
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'index.html')
})
],
externals: ['react', 'react-dom']
});
This configuration sets libraryTarget
to system
, which enables compatibility with Single-SPA’s SystemJS module loader. React and ReactDOM are marked as external dependencies so they aren’t bundled with each microfrontend.
Configuring Routing with Single-SPA Layout
Single-SPA Layout provides a way to manage routing between microfrontends, creating a seamless user experience across applications. To enable routing, update single-spa-config.js
in root-config
:
import { constructRoutes, constructApplications, constructLayoutEngine } from 'single-spa-layout';
import microfrontendLayout from './microfrontend-layout.html';
const routes = constructRoutes(microfrontendLayout);
const applications = constructApplications({ routes });
const layoutEngine = constructLayoutEngine({ routes });
start();
This setup uses a microfrontend-layout.html
file to define routes, which helps manage navigation between different microfrontends.
Best Practices for Managing Microfrontends in Nx
1. Use Shared Libraries
Create shared libraries in the /libs
directory to store components, utilities, or services that can be reused across microfrontends. This reduces code duplication and ensures consistency across applications.
2. Keep Microfrontends Independent
One of the advantages of microfrontends is that they can be developed and deployed independently. Avoid direct dependencies between microfrontends and instead use shared services or events for communication.
3. Optimize Build and Deployment
As you add more microfrontends, consider using techniques like code splitting and lazy loading to optimize performance. Nx’s caching and incremental builds can also help speed up the build process.
Testing and Debugging Single-SPA Microfrontends in Nx
Testing and debugging are essential for a stable microfrontend architecture. Here are a few ways to test and debug Single-SPA applications in an Nx monorepo:
Unit Testing with Jest
Each microfrontend should have its own unit tests, which can be configured using Jest within Nx. Unit tests help ensure that individual components and modules function as expected.
End-to-End Testing with Cypress
Cypress is useful for end-to-end testing across microfrontends. You can set up Cypress tests to simulate user interactions and validate that the applications load and interact properly.
Using Single-SPA Inspector for Debugging
The Single-SPA Inspector Chrome extension helps debug applications by showing information about each microfrontend, including their status and lifecycle events. It’s an invaluable tool for troubleshooting microfrontend loading and rendering issues.
Conclusion
Integrating Single-SPA in an Nx monorepo enables scalable and flexible microfrontend development. By following the steps outlined in this guide, you can set up a microfrontend architecture that leverages the strengths of both Single-SPA and Nx, supporting modular development, efficient dependency management, and independent deployment.
With Single-SPA and Nx, your team can benefit from increased productivity, flexibility, and maintainability in managing large-scale frontend applications. As you continue to develop and expand your microfrontend architecture, remember to follow best practices for modularization, shared libraries, and testing to maintain a cohesive and efficient development environment.