The router for easy microfrontends that enables multiple frameworks to coexist on the same page.
—
Status constants and utilities for monitoring application states and lifecycle management.
Single-spa defines status constants that represent different states in an application's lifecycle.
// Application loading states
const NOT_LOADED = "NOT_LOADED";
const LOADING_SOURCE_CODE = "LOADING_SOURCE_CODE";
const LOAD_ERROR = "LOAD_ERROR";
// Application lifecycle states
const NOT_BOOTSTRAPPED = "NOT_BOOTSTRAPPED";
const BOOTSTRAPPING = "BOOTSTRAPPING";
const NOT_MOUNTED = "NOT_MOUNTED";
const MOUNTING = "MOUNTING";
const MOUNTED = "MOUNTED";
const UNMOUNTING = "UNMOUNTING";
const UNLOADING = "UNLOADING";
// Error states
const SKIP_BECAUSE_BROKEN = "SKIP_BECAUSE_BROKEN";Usage Examples:
import {
getAppStatus,
MOUNTED,
NOT_LOADED,
SKIP_BECAUSE_BROKEN
} from "single-spa";
function checkApplicationHealth() {
const apps = ["navbar", "products", "dashboard"];
apps.forEach(appName => {
const status = getAppStatus(appName);
switch (status) {
case MOUNTED:
console.log(`${appName} is running normally`);
break;
case NOT_LOADED:
console.log(`${appName} hasn't been loaded yet`);
break;
case SKIP_BECAUSE_BROKEN:
console.error(`${appName} is broken and being skipped`);
break;
default:
console.log(`${appName} status: ${status}`);
}
});
}Gets the current lifecycle status of a specific application.
/**
* Get the current status of an application
* @param appName - Name of the application
* @returns Current status string or null if application not found
*/
function getAppStatus(appName: string): string | null;Usage Examples:
import { getAppStatus, MOUNTED, MOUNTING } from "single-spa";
// Check if application is ready
function isAppReady(appName) {
const status = getAppStatus(appName);
return status === MOUNTED;
}
// Wait for application to be mounted
async function waitForAppToMount(appName, timeout = 5000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const status = getAppStatus(appName);
if (status === MOUNTED) {
return true;
}
if (status === MOUNTING) {
await new Promise(resolve => setTimeout(resolve, 100));
continue;
}
throw new Error(`App ${appName} failed to mount (status: ${status})`);
}
throw new Error(`Timeout waiting for ${appName} to mount`);
}Returns an array of currently mounted application names.
/**
* Get names of currently mounted applications
* @returns Array of mounted application names
*/
function getMountedApps(): string[];Usage Examples:
import { getMountedApps } from "single-spa";
// Monitor currently active applications
function logActiveApplications() {
const mounted = getMountedApps();
console.log(`Currently active: ${mounted.join(", ")}`);
}
// Check if specific app is mounted
function isAppMounted(appName) {
return getMountedApps().includes(appName);
}
// Performance monitoring
function trackApplicationUsage() {
const mountedApps = getMountedApps();
analytics.track("applications_active", {
count: mountedApps.length,
apps: mountedApps
});
}Monitor application lifecycle changes using custom events:
// Listen for application status changes
window.addEventListener("single-spa:app-change", (event) => {
const {
newAppStatuses,
appsByNewStatus,
totalAppChanges
} = event.detail;
console.log("Application status changes:", newAppStatuses);
console.log("Apps by status:", appsByNewStatus);
});
// Listen for routing events that trigger app changes
window.addEventListener("single-spa:routing-event", (event) => {
console.log("Navigation event:", event.detail);
});import {
getAppStatus,
getMountedApps,
getAppNames,
MOUNTED,
SKIP_BECAUSE_BROKEN
} from "single-spa";
// Create a status dashboard
function createStatusDashboard() {
const allApps = getAppNames();
const mountedApps = getMountedApps();
const statusReport = allApps.map(appName => ({
name: appName,
status: getAppStatus(appName),
isMounted: mountedApps.includes(appName)
}));
return statusReport;
}
// Health check function
function performHealthCheck() {
const allApps = getAppNames();
const brokenApps = allApps.filter(appName =>
getAppStatus(appName) === SKIP_BECAUSE_BROKEN
);
if (brokenApps.length > 0) {
console.error("Broken applications detected:", brokenApps);
// Trigger alerts or recovery procedures
}
return {
healthy: allApps.length - brokenApps.length,
broken: brokenApps.length,
total: allApps.length
};
}Understanding the application lifecycle flow:
NOT_LOADED
↓ (when activeWhen returns true)
LOADING_SOURCE_CODE
↓ (app loaded successfully)
NOT_BOOTSTRAPPED
↓ (bootstrap called)
BOOTSTRAPPING
↓ (bootstrap complete)
NOT_MOUNTED
↓ (mount called)
MOUNTING
↓ (mount complete)
MOUNTED
↓ (update called - optional, internal state not exported)
UPDATING (internal)
↓ (update complete)
MOUNTED
↓ (when activeWhen returns false)
UNMOUNTING
↓ (unmount complete)
NOT_MOUNTED
↓ (unload called - optional)
UNLOADING
↓ (unload complete)
NOT_LOADED
// Error states can occur at any point:
LOAD_ERROR (if loading fails)
SKIP_BECAUSE_BROKEN (if any lifecycle fails)import { getAppStatus, addErrorHandler } from "single-spa";
// Retry mechanism for failed applications
class ApplicationManager {
constructor() {
this.retryAttempts = new Map();
this.maxRetries = 3;
}
async retryFailedApp(appName) {
const attempts = this.retryAttempts.get(appName) || 0;
if (attempts >= this.maxRetries) {
console.error(`Max retries exceeded for ${appName}`);
return false;
}
this.retryAttempts.set(appName, attempts + 1);
try {
// Trigger app reload/remount logic
await triggerAppChange();
// Check if app recovered
const status = getAppStatus(appName);
if (status === MOUNTED) {
this.retryAttempts.delete(appName);
return true;
}
} catch (error) {
console.error(`Retry failed for ${appName}:`, error);
}
return false;
}
}
// Circuit breaker pattern
class ApplicationCircuitBreaker {
constructor(appName, threshold = 5) {
this.appName = appName;
this.threshold = threshold;
this.failures = 0;
this.isOpen = false;
this.lastFailure = null;
}
recordSuccess() {
this.failures = 0;
this.isOpen = false;
}
recordFailure() {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.threshold) {
this.isOpen = true;
console.warn(`Circuit breaker opened for ${this.appName}`);
}
}
canProceed() {
if (!this.isOpen) return true;
// Check if enough time has passed to try again
const timeSinceLastFailure = Date.now() - this.lastFailure;
return timeSinceLastFailure > 60000; // 1 minute cooldown
}
}interface SingleSpaCustomEventDetail {
newAppStatuses: SingleSpaNewAppStatus;
appsByNewStatus: SingleSpaAppsByNewStatus;
totalAppChanges: number;
originalEvent?: Event;
oldUrl: string;
newUrl: string;
navigationIsCanceled: boolean;
cancelNavigation?: () => void;
}
interface SingleSpaNewAppStatus {
[appName: string]: "MOUNTED" | "NOT_MOUNTED" | "NOT_LOADED" | "SKIP_BECAUSE_BROKEN";
}
interface SingleSpaAppsByNewStatus {
MOUNTED: string[];
NOT_MOUNTED: string[];
NOT_LOADED: string[];
SKIP_BECAUSE_BROKEN: string[];
}Install with Tessl CLI
npx tessl i tessl/npm-single-spa