Single-Spa Framework for Micro Frontends

Loading

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

  1. Shared dependencies: Avoid duplicate library loading
  2. Code splitting: Split large microfrontends into smaller chunks
  3. Prefetching: Load non-critical MFEs during idle time
  4. Caching: Use long-term caching for MFE bundles

Organizational Patterns

  1. Vertical splitting: By business domain (products, orders, users)
  2. Horizontal splitting: By functional area (header, sidebar, content)
  3. Route-based: Each route loads a different MFE
  4. Component-based: Individual components as MFEs

Security Considerations

  1. CSP headers: Proper Content Security Policy for cross-origin MFEs
  2. Sanitization: Secure inter-MFE communication
  3. Authentication: Shared auth context
  4. 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

  1. Start each microfrontend independently:
   cd react-navbar && npm start
   cd angular-dashboard && npm start
   cd root-config && npm start
  1. Access the integrated application at http://localhost:9000
  2. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *