![]()
Single-spa is a JavaScript framework for frontend microservices that enables you to build modular, independently deployable frontend applications that work together seamlessly. Here’s how to architect and implement a micro frontend solution using single-spa.
1. Core Concepts and Architecture
Single-spa Architecture
root-config (Host)
├── app-shell (Shared components, routing)
├── @react-mf/navbar (React microfrontend)
├── @angular-mf/dashboard (Angular microfrontend)
└── @vue-mf/settings (Vue microfrontend)
Key Principles
- Independent deployment: Each microfrontend can be deployed separately
- Technology agnostic: Mix React, Vue, Angular, etc.
- Shared dependencies: Avoid duplicate library loading
- Isolated CSS: Prevent style collisions
2. Root Configuration Setup
Basic root-config
// root-config.js
import { registerApplication, start } from 'single-spa';
registerApplication({
name: '@react-mf/navbar',
app: () => System.import('@react-mf/navbar'),
activeWhen: ['/'],
customProps: {
authToken: 'xyz123'
}
});
registerApplication({
name: '@angular-mf/dashboard',
app: () => System.import('@angular-mf/dashboard'),
activeWhen: ['/dashboard']
});
start();
HTML Entry File
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Microfrontend Host</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.0.0/dist/import-map-overrides.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.0/dist/system.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.0/dist/extras/amd.min.js"></script>
</head>
<body>
<div id="navbar"></div>
<div id="content"></div>
<script type="systemjs-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
"@react-mf/navbar": "https://react-nav.mydomain.com/react-mf-navbar.js",
"@angular-mf/dashboard": "https://angular-dash.mydomain.com/angular-mf-dashboard.js"
}
}
</script>
<script src="root-config.js"></script>
</body>
</html>
3. Creating a React Microfrontend
React App Configuration
// src/root.component.js
import React from 'react';
export default function Root(props) {
return (
<section style={{ marginTop: '2em' }}>
{props.name} is mounted!
<div>{props.authToken}</div>
</section>
);
}
Single-spa Lifecycle
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import Root from './root.component';
const lifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: Root,
errorBoundary(err, info, props) {
return <div>Error: {err.message}</div>;
}
});
export const { bootstrap, mount, unmount } = lifecycles;
Webpack Config
// webpack.config.js
const singleSpaDefaults = require("webpack-config-single-spa-react");
module.exports = () => {
const defaultConfig = singleSpaDefaults({
orgName: "react-mf",
projectName: "navbar"
});
return {
...defaultConfig,
externals: ["react-router-dom"]
};
};
4. Creating an Angular Microfrontend
Angular App Setup
// main.single-spa.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { singleSpaAngular } from 'single-spa-angular';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
const lifecycles = singleSpaAngular({
bootstrapFunction: () => platformBrowserDynamic().bootstrapModule(AppModule),
template: '<angular-mf-root />',
Router: null, // Disable router if using single-spa-layout
NgZone: 'noop'
});
export const { bootstrap, mount, unmount } = lifecycles;
Angular Module Configuration
// app.module.ts
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule implements DoBootstrap {
ngDoBootstrap() {}
}
5. Cross-Microfrontend Communication
Using Custom Events
// In sender microfrontend
function sendData() {
const event = new CustomEvent('global-data-event', {
detail: { key: 'value' }
});
window.dispatchEvent(event);
}
// In receiver microfrontend
useEffect(() => {
const handler = (event) => {
console.log('Received:', event.detail);
};
window.addEventListener('global-data-event', handler);
return () => window.removeEventListener('global-data-event', handler);
}, []);
Using Shared State (Redux)
// shared-dependencies.js
import { createStore } from 'redux';
const initialState = { user: null };
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
default:
return state;
}
};
export const store = createStore(reducer);
// In import-map-overrides
{
"imports": {
"@shared/store": "shared-dependencies.js"
}
}
6. Shared Dependencies and Performance
Import Map Configuration
<script type="systemjs-importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js",
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
"@react-mf/navbar": "//localhost:8500/react-mf-navbar.js",
"@angular-mf/dashboard": "//localhost:8501/angular-mf-dashboard.js"
}
}
</script>
Webpack Externals
// In each microfrontend's webpack.config.js
module.exports = {
// ...
externals: ['react', 'react-dom', 'single-spa']
};
7. Routing Between Microfrontends
Single-spa Layout Engine
// root-config.js
import { registerApplication, start } from 'single-spa';
import { constructApplications, constructRoutes, constructLayoutEngine } from 'single-spa-layout';
const routes = constructRoutes(`
<single-spa-router>
<route default>
<application name="@react-mf/navbar"></application>
<div class="content">
<route path="dashboard">
<application name="@angular-mf/dashboard"></application>
</route>
<route path="settings">
<application name="@vue-mf/settings"></application>
</route>
</div>
</route>
</single-spa-router>
`);
const applications = constructApplications({
routes,
loadApp({ name }) {
return System.import(name);
}
});
const layoutEngine = constructLayoutEngine({ routes, applications });
applications.forEach(registerApplication);
start();
8. Deployment Strategies
Independent Deployment
https://cdn.example.com/microfrontends/
├── react-navbar/1.0.0/main.js
├── react-navbar/1.1.0/main.js
├── angular-dashboard/2.3.0/main.js
└── vue-settings/1.5.0/main.js
Versioned Import Maps
// Dynamic import map loading
fetch('https://cdn.example.com/importmap.json')
.then(res => res.json())
.then(importMap => {
const im = document.createElement('script');
im.type = 'systemjs-importmap';
im.textContent = JSON.stringify(importMap);
document.head.appendChild(im);
// Load systemjs after import map is set
const s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/systemjs@6.8.0/dist/system.min.js';
s.onload = () => System.import('root-config.js');
document.head.appendChild(s);
});
9. Testing Microfrontends
Integration Testing
describe('Microfrontend Integration', () => {
beforeAll(() => {
// Load the root config and all microfrontends
System.import('root-config.js');
});
it('should mount the navbar', async () => {
window.history.pushState({}, '', '/');
await sleep(500); // Wait for mounting
expect(document.querySelector('#navbar')).toBeTruthy();
});
});
Mocking Other Microfrontends
// In test setup
System.set('@react-mf/navbar', {
bootstrap: () => Promise.resolve(),
mount: () => Promise.resolve(),
unmount: () => Promise.resolve()
});
10. Best Practices and Patterns
Performance Optimization
- Shared dependencies: Avoid duplicate library loading
- Code splitting: Split large microfrontends into smaller chunks
- Prefetching: Load non-critical MFEs during idle time
- Caching: Use long-term caching for MFE bundles
Organizational Patterns
- Vertical splitting: By business domain (products, orders, users)
- Horizontal splitting: By functional area (header, sidebar, content)
- Route-based: Each route loads a different MFE
- Component-based: Individual components as MFEs
Security Considerations
- CSP headers: Proper Content Security Policy for cross-origin MFEs
- Sanitization: Secure inter-MFE communication
- Authentication: Shared auth context
- Error boundaries: Isolate failures
Complete Example Structure
Project Structure
microfrontends/
├── root-config/ # Host application
│ ├── src/
│ │ ├── index.ejs # HTML template
│ │ └── root-config.js # Single-spa config
│ └── package.json
├── react-navbar/ # React microfrontend
│ ├── src/
│ │ ├── root.component.js
│ │ └── index.js
│ └── package.json
├── angular-dashboard/ # Angular microfrontend
│ ├── src/
│ │ ├── app/
│ │ └── main.single-spa.ts
│ └── package.json
└── vue-settings/ # Vue microfrontend
├── src/
│ ├── App.vue
│ └── main.js
└── package.json
Development Workflow
- Start each microfrontend independently:
cd react-navbar && npm start
cd angular-dashboard && npm start
cd root-config && npm start
- Access the integrated application at
http://localhost:9000 - Make changes in any MFE and see hot reloading in action
By implementing microfrontends with single-spa following these patterns, you can create a modular, scalable frontend architecture that enables independent development, deployment, and scaling of different parts of your application. The framework’s flexibility allows you to mix technologies while maintaining a cohesive user experience.
