The router for easy microfrontends that enables multiple frameworks to coexist on the same page.
—
Manual component management system for mounting components outside of the standard application lifecycle. Parcels provide a way to manually control when and where components are mounted.
Mounts a parcel at the root level, not within a specific application. This is useful for shared components like modals, tooltips, or notifications that need to be managed independently.
/**
* Mount a parcel at the root level
* @param parcelConfig - Parcel configuration object or loading function
* @param parcelProps - Properties including DOM element and custom props
* @returns Parcel instance with lifecycle methods
*/
function mountRootParcel(
parcelConfig: ParcelConfig,
parcelProps: ParcelProps & CustomProps
): Parcel;
type ParcelConfig = ParcelConfigObject | (() => Promise<ParcelConfigObject>);
interface ParcelConfigObject {
name?: string;
bootstrap: LifeCycleFn | Array<LifeCycleFn>;
mount: LifeCycleFn | Array<LifeCycleFn>;
unmount: LifeCycleFn | Array<LifeCycleFn>;
update?: LifeCycleFn | Array<LifeCycleFn>;
}
interface ParcelProps {
domElement: HTMLElement;
}
interface Parcel {
mount(): Promise<null>;
unmount(): Promise<null>;
update?(customProps: CustomProps): Promise<any>;
getStatus(): ParcelStatus;
loadPromise: Promise<null>;
bootstrapPromise: Promise<null>;
mountPromise: Promise<null>;
unmountPromise: Promise<null>;
}
type ParcelStatus =
| "NOT_LOADED"
| "LOADING_SOURCE_CODE"
| "NOT_BOOTSTRAPPED"
| "BOOTSTRAPPING"
| "NOT_MOUNTED"
| "MOUNTING"
| "MOUNTED"
| "UNMOUNTING"
| "UNLOADING"
| "SKIP_BECAUSE_BROKEN"
| "LOAD_ERROR";Usage Examples:
import { mountRootParcel } from "single-spa";
// Mount a simple parcel
const modalContainer = document.getElementById("modal-container");
const modalParcel = mountRootParcel(
() => import("./modal/modal.parcel.js"),
{
domElement: modalContainer,
title: "User Settings",
onClose: () => modalParcel.unmount()
}
);
// Mount parcel with configuration object
const tooltipParcel = mountRootParcel(
{
name: "tooltip",
bootstrap: () => Promise.resolve(),
mount: (props) => {
props.domElement.innerHTML = `<div class="tooltip">${props.text}</div>`;
return Promise.resolve();
},
unmount: (props) => {
props.domElement.innerHTML = "";
return Promise.resolve();
}
},
{
domElement: document.getElementById("tooltip"),
text: "This is a tooltip"
}
);
// Mount parcel with lifecycle management
async function createNotificationParcel(message, type = "info") {
const container = document.createElement("div");
document.body.appendChild(container);
const parcel = mountRootParcel(
() => import("./notification/notification.parcel.js"),
{
domElement: container,
message,
type,
onDismiss: async () => {
await parcel.unmount();
document.body.removeChild(container);
}
}
);
// Auto-dismiss after 5 seconds
setTimeout(async () => {
if (parcel.getStatus() === "MOUNTED") {
await parcel.unmount();
document.body.removeChild(container);
}
}, 5000);
return parcel;
}Parcels follow a similar lifecycle to applications but are manually controlled:
import { mountRootParcel } from "single-spa";
async function manageParcels() {
const container = document.getElementById("dynamic-content");
// Create parcel
const parcel = mountRootParcel(
() => import("./widget/widget.parcel.js"),
{
domElement: container,
initialData: { userId: 123 }
}
);
// Wait for parcel to be mounted
await parcel.mountPromise;
console.log("Parcel mounted successfully");
// Update parcel with new data
if (parcel.update) {
await parcel.update({ userId: 456, theme: "dark" });
console.log("Parcel updated");
}
// Check parcel status
const status = parcel.getStatus();
if (status === "MOUNTED") {
console.log("Parcel is ready");
}
// Unmount when done
await parcel.unmount();
console.log("Parcel unmounted");
}Applications can also mount parcels using the mountParcel function provided in their props:
// Within an application's mount lifecycle
export async function mount(props) {
const { domElement, mountParcel } = props;
// Mount a parcel within this application
const widgetContainer = domElement.querySelector("#widget-container");
const widgetParcel = mountParcel(
() => import("./widget/widget.parcel.js"),
{
domElement: widgetContainer,
parentApp: props.name
}
);
// Store parcel reference for cleanup
domElement.widgetParcel = widgetParcel;
return Promise.resolve();
}
export async function unmount(props) {
const { domElement } = props;
// Clean up parcel
if (domElement.widgetParcel) {
await domElement.widgetParcel.unmount();
delete domElement.widgetParcel;
}
return Promise.resolve();
}Create a factory for commonly used parcels:
import { mountRootParcel } from "single-spa";
class ParcelFactory {
static async createModal(config) {
const container = document.createElement("div");
container.className = "modal-backdrop";
document.body.appendChild(container);
const parcel = mountRootParcel(
() => import("./modal/modal.parcel.js"),
{
domElement: container,
...config,
onClose: async () => {
await parcel.unmount();
document.body.removeChild(container);
if (config.onClose) config.onClose();
}
}
);
return parcel;
}
static async createToast(message, type = "info", duration = 3000) {
const container = document.createElement("div");
container.className = "toast-container";
document.body.appendChild(container);
const parcel = mountRootParcel(
() => import("./toast/toast.parcel.js"),
{
domElement: container,
message,
type
}
);
// Auto-remove toast
setTimeout(async () => {
await parcel.unmount();
document.body.removeChild(container);
}, duration);
return parcel;
}
}
// Usage
const confirmModal = await ParcelFactory.createModal({
title: "Confirm Action",
message: "Are you sure you want to delete this item?",
onConfirm: () => console.log("Confirmed"),
onClose: () => console.log("Modal closed")
});
const successToast = await ParcelFactory.createToast(
"Item saved successfully!",
"success",
2000
);Manage multiple parcels with a registry pattern:
import { mountRootParcel } from "single-spa";
class ParcelRegistry {
constructor() {
this.parcels = new Map();
}
async register(id, parcelConfig, parcelProps) {
if (this.parcels.has(id)) {
throw new Error(`Parcel with id "${id}" already exists`);
}
const parcel = mountRootParcel(parcelConfig, parcelProps);
this.parcels.set(id, parcel);
return parcel;
}
async unregister(id) {
const parcel = this.parcels.get(id);
if (!parcel) {
console.warn(`Parcel with id "${id}" not found`);
return;
}
await parcel.unmount();
this.parcels.delete(id);
}
get(id) {
return this.parcels.get(id);
}
async unregisterAll() {
const promises = Array.from(this.parcels.values()).map(parcel =>
parcel.unmount().catch(console.error)
);
await Promise.all(promises);
this.parcels.clear();
}
getStatus(id) {
const parcel = this.parcels.get(id);
return parcel ? parcel.getStatus() : null;
}
getAllStatuses() {
const statuses = {};
this.parcels.forEach((parcel, id) => {
statuses[id] = parcel.getStatus();
});
return statuses;
}
}
// Usage
const parcelRegistry = new ParcelRegistry();
// Register parcels
await parcelRegistry.register(
"notification",
() => import("./notification/notification.parcel.js"),
{ domElement: document.getElementById("notifications") }
);
await parcelRegistry.register(
"sidebar",
() => import("./sidebar/sidebar.parcel.js"),
{ domElement: document.getElementById("sidebar") }
);
// Later, clean up specific parcel
await parcelRegistry.unregister("notification");
// Or clean up all parcels
await parcelRegistry.unregisterAll();import { mountRootParcel, addErrorHandler } from "single-spa";
// Handle parcel errors
addErrorHandler((error) => {
if (error.appOrParcelName && error.appOrParcelName.includes("parcel")) {
console.error("Parcel error:", error);
// Handle parcel-specific errors
}
});
// Safe parcel mounting with error handling
async function safeMountParcel(config, props) {
try {
const parcel = mountRootParcel(config, props);
await parcel.mountPromise;
return parcel;
} catch (error) {
console.error("Failed to mount parcel:", error);
// Clean up DOM element if needed
if (props.domElement && props.domElement.parentNode) {
props.domElement.parentNode.removeChild(props.domElement);
}
throw error;
}
}interface CustomProps {
[key: string]: any;
[key: number]: any;
}
type LifeCycleFn = (props: ParcelProps & CustomProps) => Promise<any>;Install with Tessl CLI
npx tessl i tessl/npm-single-spa