Skip to content

Single-SPA Cheat Sheet

single-spa orchestrates multiple frontend microfrontends in one browser tab: a root config registers apps, routes activate them, and lifecycles mount/unmount each framework.

How to use this Single-SPA cheat sheet

registerApplication(name, loadApp, activeWhen) tells the root config when to fetch and bootstrap each microfrontend. Lifecycles bootstrap, mount, and unmount integrate React, Vue, or Angular apps into shared DOM containers.

Parcels are mountable widgets without routing—useful for embedding one app inside another. CSS isolation relies on scoped styles, shadow DOM, or naming conventions; shared dependencies reduce duplicate React loads via import maps or module federation.

Quick Single-SPA example

import { registerApplication, start } from 'single-spa';

registerApplication({
  name: '@org/navbar',
  app: () => System.import('@org/navbar'),
  activeWhen: ['/'],
});

registerApplication({
  name: '@org/dashboard',
  app: () => System.import('@org/dashboard'),
  activeWhen: (location) => location.pathname.startsWith('/dashboard'),
});

start();

Root Config & registerApplication

Option Example Notes
name "@org/app" Unique string identifier
app () => System.import("@org/app") Lazy load microfrontend bundle
activeWhen "/settings" or function When app should mount
customProps customProps: { authToken } Pass shared data to lifecycles
start() start({ urlRerouteOnly: true }) Begin listening to routing
Layout engine single-spa-layout routes + slots Declarative DOM placement
Import map <script type="systemjs-importmap"> Resolve bare specifiers in browser
index.html One root config entry script Host page loads orchestrator only

Application Lifecycles

Lifecycle Example Purpose
bootstrap export async function bootstrap(props) One-time setup before first mount
mount export async function mount(props) Render into DOM container
unmount export async function unmount(props) Teardown when route deactivates
unload Optional; rarely used After unmount when app may not return
timeout timeouts: { mount: { millis: 5000 } } Fail fast on hung mounts
single-spa-react singleSpaReact({ React, ReactDOM, rootComponent }) Generates lifecycles for React
single-spa-vue singleSpaVue({ Vue, appOptions }) Vue 2/3 adapter
Error boundary Catch mount failures in root Show fallback UI per app

Parcels & CSS Isolation

Topic Example Notes
mountRootParcel mountRootParcel(parcelConfig, props) Embed microfrontend in another app
Parcel lifecycles Same bootstrap/mount/unmount No activeWhen routing
Unmount parcel parcel.unmount() Clean up when parent unmounts
CSS modules styles.button Scoped class names per build
BEM prefix .app-dashboard__btn Namespace global CSS
Shadow DOM Web components wrapper Strong isolation; tricky with global tokens
Shared design tokens CSS variables on :root Coordinate across microfrontends
Z-index scale Document layer stack Prevent modal/overlay conflicts

Routing & Shared Dependencies

Pattern Example Notes
History API single-spa listens to popstate Use pushState in apps consistently
Base path activeWhen: "/app1" Each app owns URL prefix
Default route Redirect / to primary app Root config or layout handles
Shared React Webpack externals + import map One React copy in browser
Version mismatch Two Reacts break hooks Align peer dependency versions
Module federation Host loads remoteEntry.js Alternative to SystemJS import maps
Cross-app comms Custom events or shared store Avoid tight coupling; prefer URL state
Local dev Import map overrides in devtools Point to localhost webpack dev server

React microfrontend lifecycles

import React from 'react';
import ReactDOMClient from 'react-dom/client';
import singleSpaReact from 'single-spa-react';
import App from './App';

const lifecycles = singleSpaReact({
  React,
  ReactDOMClient,
  rootComponent: App,
  errorBoundary(err) => React.createElement('div', null, 'App crashed'),
});

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

Parcel and application configs share the same lifecycle shape.

Layout route with single-spa-layout

<single-spa-router>
  <application name="@org/nav"></application>
  <route path="dashboard">
    <application name="@org/dashboard"></application>
  </route>
</single-spa-router>

Layout engine mounts apps into designated DOM nodes.

Import map for shared dependencies

<script type="systemjs-importmap">
{
  "imports": {
    "react": "https://cdn.example/react@18.3.1/production.min.js",
    "react-dom": "https://cdn.example/react-dom@18.3.1/production.min.js",
    "@org/navbar": "https://cdn.example/navbar/main.js"
  }
}
</script>

Pin exact versions; mismatch across apps causes subtle runtime bugs.

Common mistakes

  • Loading two copies of React because externals and import maps were not aligned.
  • Global CSS from one microfrontend leaking styles into another without namespacing.
  • Forgetting unmount cleanup—event listeners and timers survive route changes.
  • Using activeWhen string match when pathname includes locale prefix (/en/app).

Key takeaways

  • Root config registers apps with activeWhen; start() enables routing.
  • bootstrap/mount/unmount integrate any framework via adapters.
  • Parcels embed apps without route ownership.
  • Isolate CSS and share one copy of major libraries via import maps or federation.

Pro Tip

Document a z-index and modal portal convention across teams before the first overlay bug ships to production.