Skip to main content
Technology & EngineeringMicro Frontend241 lines

Single Spa

Single-SPA framework for orchestrating multiple JavaScript micro-frontends within a single page application shell.

Quick Summary24 lines
You are an expert in the Single-SPA framework for building micro-frontend architectures. You help teams orchestrate multiple independent frontend applications — potentially using different frameworks — within a unified single-page application shell.

## Key Points

- **Root config** — the top-level HTML page and JavaScript that registers all micro-frontend applications.
- **Application** — a full micro-frontend bound to a route prefix; mounts and unmounts as the user navigates.
- **Parcel** — a framework-agnostic component that can be mounted imperatively anywhere, not tied to a route.
- **Utility module** — shared code (auth, logging, state) loaded via an import map but not mounted to the DOM.
1. **bootstrap** — called once, the first time the app is about to mount. Use it for one-time initialization.
2. **mount** — called each time the app should render into the DOM. Receives a `props` object including the DOM element.
3. **unmount** — called each time the app should clean up and remove itself from the DOM.
1. **Use a layout engine** — `single-spa-layout` lets you define application placement declaratively in HTML templates instead of scattering `registerApplication` calls.
2. **Keep the root config thin** — it should only register apps and configure shared services. No business logic belongs here.
3. **Externalize shared frameworks** — put React, Vue, Angular in the import map so all apps share the same instance.
4. **One team per application** — align micro-frontend boundaries with team ownership for true independent delivery.
5. **Use utility modules for cross-cutting concerns** — auth, analytics, feature flags, and logging should be utility modules, not duplicated.

## Quick Example

```js
(location) => location.pathname.startsWith("/catalog");
```
skilldb get micro-frontend-skills/Single SpaFull skill: 241 lines
Paste into your CLAUDE.md or agent config

Single-SPA — Micro-Frontends

You are an expert in the Single-SPA framework for building micro-frontend architectures. You help teams orchestrate multiple independent frontend applications — potentially using different frameworks — within a unified single-page application shell.

Overview

Single-SPA is a JavaScript meta-framework that enables multiple frontend applications (called "parcels" or "applications") to coexist on the same page. Each application has its own lifecycle (bootstrap, mount, unmount) and can be written in any framework. A root config orchestrates which applications are active based on the current route.

Key terminology:

  • Root config — the top-level HTML page and JavaScript that registers all micro-frontend applications.
  • Application — a full micro-frontend bound to a route prefix; mounts and unmounts as the user navigates.
  • Parcel — a framework-agnostic component that can be mounted imperatively anywhere, not tied to a route.
  • Utility module — shared code (auth, logging, state) loaded via an import map but not mounted to the DOM.

Core Concepts

Lifecycle Functions

Every single-spa application must export three async lifecycle functions:

  1. bootstrap — called once, the first time the app is about to mount. Use it for one-time initialization.
  2. mount — called each time the app should render into the DOM. Receives a props object including the DOM element.
  3. unmount — called each time the app should clean up and remove itself from the DOM.

Activity Functions

The root config provides an "activity function" per application that returns true when the app should be active:

(location) => location.pathname.startsWith("/catalog");

Single-SPA evaluates these on every route change and mounts/unmounts apps accordingly.

Import Maps and SystemJS

Single-SPA recommends using browser-native import maps (or SystemJS as a polyfill) so each micro-frontend is referenced by URL and can be deployed independently without rebuilding the shell.

Implementation Patterns

Root Config (HTML)

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="importmap-type" content="systemjs-importmap" />
  <script type="systemjs-importmap">
    {
      "imports": {
        "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5/lib/system/single-spa.min.js",
        "@myorg/root-config": "/root-config.js",
        "@myorg/navbar": "https://navbar.example.com/main.js",
        "@myorg/catalog": "https://catalog.example.com/main.js",
        "@myorg/checkout": "https://checkout.example.com/main.js"
      }
    }
  </script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs/dist/extras/amd.min.js"></script>
</head>
<body>
  <div id="navbar"></div>
  <div id="main-content"></div>
  <script>
    System.import("@myorg/root-config");
  </script>
</body>
</html>

Root Config (JavaScript)

// root-config.js
import { registerApplication, start } from "single-spa";

registerApplication({
  name: "@myorg/navbar",
  app: () => System.import("@myorg/navbar"),
  activeWhen: "/",              // always active
  customProps: { domElement: document.getElementById("navbar") },
});

registerApplication({
  name: "@myorg/catalog",
  app: () => System.import("@myorg/catalog"),
  activeWhen: "/catalog",
  customProps: { domElement: document.getElementById("main-content") },
});

registerApplication({
  name: "@myorg/checkout",
  app: () => System.import("@myorg/checkout"),
  activeWhen: "/checkout",
  customProps: { domElement: document.getElementById("main-content") },
});

start();

React Micro-Frontend Application

// catalog/src/myorg-catalog.jsx
import React from "react";
import { createRoot } from "react-dom/client";
import singleSpaReact from "single-spa-react";
import CatalogApp from "./CatalogApp";

const lifecycles = singleSpaReact({
  React,
  createRoot,
  rootComponent: CatalogApp,
  errorBoundary(err, info, props) {
    return <div>Catalog failed to load.</div>;
  },
});

export const { bootstrap, mount, unmount } = lifecycles;

Vue Micro-Frontend Application

// checkout/src/main.js
import { h, createApp } from "vue";
import singleSpaVue from "single-spa-vue";
import CheckoutApp from "./CheckoutApp.vue";

const vueLifecycles = singleSpaVue({
  createApp,
  appOptions: {
    render() {
      return h(CheckoutApp, { name: this.name });
    },
  },
});

export const { bootstrap, mount, unmount } = vueLifecycles;

Using Parcels for Cross-Framework Embedding

// Inside a React app, mount a Vue parcel
import Parcel from "single-spa-react/parcel";

function CheckoutButton() {
  return (
    <Parcel
      config={() => System.import("@myorg/vue-checkout-widget")}
      wrapWith="div"
      wrapStyle={{ display: "inline-block" }}
    />
  );
}

Utility Module (Shared Auth)

// auth/src/myorg-auth.js — utility module, no lifecycle
import { BehaviorSubject } from "rxjs";

const user$ = new BehaviorSubject(null);

export function getUser() {
  return user$.getValue();
}

export function login(token) {
  const decoded = decodeJwt(token);
  user$.next(decoded);
}

export function onUserChange(callback) {
  return user$.subscribe(callback);
}

Webpack Config for SystemJS Output

// webpack.config.js — each micro-frontend
module.exports = {
  output: {
    libraryTarget: "system",
    filename: "main.js",
    publicPath: "https://catalog.example.com/",
  },
  externals: ["single-spa", "react", "react-dom"],
};

Best Practices

  1. Use a layout enginesingle-spa-layout lets you define application placement declaratively in HTML templates instead of scattering registerApplication calls.
  2. Keep the root config thin — it should only register apps and configure shared services. No business logic belongs here.
  3. Externalize shared frameworks — put React, Vue, Angular in the import map so all apps share the same instance.
  4. One team per application — align micro-frontend boundaries with team ownership for true independent delivery.
  5. Use utility modules for cross-cutting concerns — auth, analytics, feature flags, and logging should be utility modules, not duplicated.
  6. Unmount cleanly — ensure every event listener, interval, and subscription is torn down in unmount to prevent memory leaks.
  7. Test each app standalone — every micro-frontend should be runnable in isolation with a dev harness, not only inside the shell.

Common Pitfalls

  • Global CSS collisions — each app renders into the same document. Without scoped styles or Shadow DOM, CSS rules bleed across apps.
  • Zombie event listeners — forgetting to clean up in unmount causes handlers from unmounted apps to keep firing.
  • Slow first load — loading many independent bundles sequentially is slow. Use <link rel="preload"> or a service worker to prefetch critical remotes.
  • Shared dependency version drift — if the import map pins React 18 but an app was built expecting React 17 internals, subtle bugs emerge.
  • Overusing parcels — parcels add complexity. Prefer plain applications bound to routes; use parcels only when you truly need cross-framework embedding.
  • No error isolation — an unhandled error in one app's mount can prevent the shell from loading others. Always wrap lifecycles in error boundaries.

Core Philosophy

Single-SPA solves the orchestration problem: how do you mount, unmount, and coordinate multiple independent frontend applications on a single page? Its lifecycle model (bootstrap, mount, unmount) provides a clean contract that works across frameworks. A React app, a Vue app, and an Angular app can coexist on the same page, each mounting and unmounting as the user navigates, without knowing about each other.

Import maps are the deployment mechanism that makes Single-SPA practical. By referencing each micro-frontend by URL in an import map, the root config can load the latest version of any micro-frontend without rebuilding or redeploying the shell. Updating a micro-frontend is as simple as changing its URL in the import map and invalidating the CDN cache. This is the operational foundation of independent deployment.

The root config should be the thinnest possible layer. It registers applications, provides the import map, and starts Single-SPA. Business logic, shared UI components, and authentication should live in utility modules loaded via the import map, not in the root config itself. A fat root config becomes a bottleneck that every team must coordinate with, recreating the monolith problem at a different level.

Anti-Patterns

  • Not cleaning up in unmount — failing to remove event listeners, clear intervals, and unsubscribe from stores in the unmount lifecycle causes memory leaks and ghost handlers that fire against detached DOM elements.

  • Loading all micro-frontend bundles on initial page load — importing every registered application eagerly defeats lazy loading; rely on activity functions to load bundles only when the user navigates to the matching route.

  • Using global CSS without scoping — each Single-SPA application renders into the same document; unscoped CSS rules leak across applications, causing visual bugs that are difficult to trace.

  • Putting business logic in the root config — the root config should only register applications, configure shared services, and start Single-SPA; embedding logic here creates a coordination bottleneck.

  • Not wrapping lifecycle functions in error boundaries — an unhandled error during one application's mount can prevent the shell from loading other applications; always catch and handle lifecycle errors gracefully.

Install this skill directly: skilldb add micro-frontend-skills

Get CLI access →