0
# Parcels System
1
2
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.
3
4
## Capabilities
5
6
### Mount Root Parcel
7
8
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.
9
10
```javascript { .api }
11
/**
12
* Mount a parcel at the root level
13
* @param parcelConfig - Parcel configuration object or loading function
14
* @param parcelProps - Properties including DOM element and custom props
15
* @returns Parcel instance with lifecycle methods
16
*/
17
function mountRootParcel(
18
parcelConfig: ParcelConfig,
19
parcelProps: ParcelProps & CustomProps
20
): Parcel;
21
22
type ParcelConfig = ParcelConfigObject | (() => Promise<ParcelConfigObject>);
23
24
interface ParcelConfigObject {
25
name?: string;
26
bootstrap: LifeCycleFn | Array<LifeCycleFn>;
27
mount: LifeCycleFn | Array<LifeCycleFn>;
28
unmount: LifeCycleFn | Array<LifeCycleFn>;
29
update?: LifeCycleFn | Array<LifeCycleFn>;
30
}
31
32
interface ParcelProps {
33
domElement: HTMLElement;
34
}
35
36
interface Parcel {
37
mount(): Promise<null>;
38
unmount(): Promise<null>;
39
update?(customProps: CustomProps): Promise<any>;
40
getStatus(): ParcelStatus;
41
loadPromise: Promise<null>;
42
bootstrapPromise: Promise<null>;
43
mountPromise: Promise<null>;
44
unmountPromise: Promise<null>;
45
}
46
47
type ParcelStatus =
48
| "NOT_LOADED"
49
| "LOADING_SOURCE_CODE"
50
| "NOT_BOOTSTRAPPED"
51
| "BOOTSTRAPPING"
52
| "NOT_MOUNTED"
53
| "MOUNTING"
54
| "MOUNTED"
55
| "UNMOUNTING"
56
| "UNLOADING"
57
| "SKIP_BECAUSE_BROKEN"
58
| "LOAD_ERROR";
59
```
60
61
**Usage Examples:**
62
63
```javascript
64
import { mountRootParcel } from "single-spa";
65
66
// Mount a simple parcel
67
const modalContainer = document.getElementById("modal-container");
68
const modalParcel = mountRootParcel(
69
() => import("./modal/modal.parcel.js"),
70
{
71
domElement: modalContainer,
72
title: "User Settings",
73
onClose: () => modalParcel.unmount()
74
}
75
);
76
77
// Mount parcel with configuration object
78
const tooltipParcel = mountRootParcel(
79
{
80
name: "tooltip",
81
bootstrap: () => Promise.resolve(),
82
mount: (props) => {
83
props.domElement.innerHTML = `<div class="tooltip">${props.text}</div>`;
84
return Promise.resolve();
85
},
86
unmount: (props) => {
87
props.domElement.innerHTML = "";
88
return Promise.resolve();
89
}
90
},
91
{
92
domElement: document.getElementById("tooltip"),
93
text: "This is a tooltip"
94
}
95
);
96
97
// Mount parcel with lifecycle management
98
async function createNotificationParcel(message, type = "info") {
99
const container = document.createElement("div");
100
document.body.appendChild(container);
101
102
const parcel = mountRootParcel(
103
() => import("./notification/notification.parcel.js"),
104
{
105
domElement: container,
106
message,
107
type,
108
onDismiss: async () => {
109
await parcel.unmount();
110
document.body.removeChild(container);
111
}
112
}
113
);
114
115
// Auto-dismiss after 5 seconds
116
setTimeout(async () => {
117
if (parcel.getStatus() === "MOUNTED") {
118
await parcel.unmount();
119
document.body.removeChild(container);
120
}
121
}, 5000);
122
123
return parcel;
124
}
125
```
126
127
### Parcel Lifecycle Management
128
129
Parcels follow a similar lifecycle to applications but are manually controlled:
130
131
```javascript
132
import { mountRootParcel } from "single-spa";
133
134
async function manageParcels() {
135
const container = document.getElementById("dynamic-content");
136
137
// Create parcel
138
const parcel = mountRootParcel(
139
() => import("./widget/widget.parcel.js"),
140
{
141
domElement: container,
142
initialData: { userId: 123 }
143
}
144
);
145
146
// Wait for parcel to be mounted
147
await parcel.mountPromise;
148
console.log("Parcel mounted successfully");
149
150
// Update parcel with new data
151
if (parcel.update) {
152
await parcel.update({ userId: 456, theme: "dark" });
153
console.log("Parcel updated");
154
}
155
156
// Check parcel status
157
const status = parcel.getStatus();
158
if (status === "MOUNTED") {
159
console.log("Parcel is ready");
160
}
161
162
// Unmount when done
163
await parcel.unmount();
164
console.log("Parcel unmounted");
165
}
166
```
167
168
### Application-Scoped Parcels
169
170
Applications can also mount parcels using the `mountParcel` function provided in their props:
171
172
```javascript
173
// Within an application's mount lifecycle
174
export async function mount(props) {
175
const { domElement, mountParcel } = props;
176
177
// Mount a parcel within this application
178
const widgetContainer = domElement.querySelector("#widget-container");
179
const widgetParcel = mountParcel(
180
() => import("./widget/widget.parcel.js"),
181
{
182
domElement: widgetContainer,
183
parentApp: props.name
184
}
185
);
186
187
// Store parcel reference for cleanup
188
domElement.widgetParcel = widgetParcel;
189
190
return Promise.resolve();
191
}
192
193
export async function unmount(props) {
194
const { domElement } = props;
195
196
// Clean up parcel
197
if (domElement.widgetParcel) {
198
await domElement.widgetParcel.unmount();
199
delete domElement.widgetParcel;
200
}
201
202
return Promise.resolve();
203
}
204
```
205
206
## Advanced Parcel Patterns
207
208
### Parcel Factory
209
210
Create a factory for commonly used parcels:
211
212
```javascript
213
import { mountRootParcel } from "single-spa";
214
215
class ParcelFactory {
216
static async createModal(config) {
217
const container = document.createElement("div");
218
container.className = "modal-backdrop";
219
document.body.appendChild(container);
220
221
const parcel = mountRootParcel(
222
() => import("./modal/modal.parcel.js"),
223
{
224
domElement: container,
225
...config,
226
onClose: async () => {
227
await parcel.unmount();
228
document.body.removeChild(container);
229
if (config.onClose) config.onClose();
230
}
231
}
232
);
233
234
return parcel;
235
}
236
237
static async createToast(message, type = "info", duration = 3000) {
238
const container = document.createElement("div");
239
container.className = "toast-container";
240
document.body.appendChild(container);
241
242
const parcel = mountRootParcel(
243
() => import("./toast/toast.parcel.js"),
244
{
245
domElement: container,
246
message,
247
type
248
}
249
);
250
251
// Auto-remove toast
252
setTimeout(async () => {
253
await parcel.unmount();
254
document.body.removeChild(container);
255
}, duration);
256
257
return parcel;
258
}
259
}
260
261
// Usage
262
const confirmModal = await ParcelFactory.createModal({
263
title: "Confirm Action",
264
message: "Are you sure you want to delete this item?",
265
onConfirm: () => console.log("Confirmed"),
266
onClose: () => console.log("Modal closed")
267
});
268
269
const successToast = await ParcelFactory.createToast(
270
"Item saved successfully!",
271
"success",
272
2000
273
);
274
```
275
276
### Parcel Registry
277
278
Manage multiple parcels with a registry pattern:
279
280
```javascript
281
import { mountRootParcel } from "single-spa";
282
283
class ParcelRegistry {
284
constructor() {
285
this.parcels = new Map();
286
}
287
288
async register(id, parcelConfig, parcelProps) {
289
if (this.parcels.has(id)) {
290
throw new Error(`Parcel with id "${id}" already exists`);
291
}
292
293
const parcel = mountRootParcel(parcelConfig, parcelProps);
294
this.parcels.set(id, parcel);
295
296
return parcel;
297
}
298
299
async unregister(id) {
300
const parcel = this.parcels.get(id);
301
if (!parcel) {
302
console.warn(`Parcel with id "${id}" not found`);
303
return;
304
}
305
306
await parcel.unmount();
307
this.parcels.delete(id);
308
}
309
310
get(id) {
311
return this.parcels.get(id);
312
}
313
314
async unregisterAll() {
315
const promises = Array.from(this.parcels.values()).map(parcel =>
316
parcel.unmount().catch(console.error)
317
);
318
await Promise.all(promises);
319
this.parcels.clear();
320
}
321
322
getStatus(id) {
323
const parcel = this.parcels.get(id);
324
return parcel ? parcel.getStatus() : null;
325
}
326
327
getAllStatuses() {
328
const statuses = {};
329
this.parcels.forEach((parcel, id) => {
330
statuses[id] = parcel.getStatus();
331
});
332
return statuses;
333
}
334
}
335
336
// Usage
337
const parcelRegistry = new ParcelRegistry();
338
339
// Register parcels
340
await parcelRegistry.register(
341
"notification",
342
() => import("./notification/notification.parcel.js"),
343
{ domElement: document.getElementById("notifications") }
344
);
345
346
await parcelRegistry.register(
347
"sidebar",
348
() => import("./sidebar/sidebar.parcel.js"),
349
{ domElement: document.getElementById("sidebar") }
350
);
351
352
// Later, clean up specific parcel
353
await parcelRegistry.unregister("notification");
354
355
// Or clean up all parcels
356
await parcelRegistry.unregisterAll();
357
```
358
359
## Error Handling
360
361
```javascript
362
import { mountRootParcel, addErrorHandler } from "single-spa";
363
364
// Handle parcel errors
365
addErrorHandler((error) => {
366
if (error.appOrParcelName && error.appOrParcelName.includes("parcel")) {
367
console.error("Parcel error:", error);
368
// Handle parcel-specific errors
369
}
370
});
371
372
// Safe parcel mounting with error handling
373
async function safeMountParcel(config, props) {
374
try {
375
const parcel = mountRootParcel(config, props);
376
await parcel.mountPromise;
377
return parcel;
378
} catch (error) {
379
console.error("Failed to mount parcel:", error);
380
// Clean up DOM element if needed
381
if (props.domElement && props.domElement.parentNode) {
382
props.domElement.parentNode.removeChild(props.domElement);
383
}
384
throw error;
385
}
386
}
387
```
388
389
## Types
390
391
```javascript { .api }
392
interface CustomProps {
393
[key: string]: any;
394
[key: number]: any;
395
}
396
397
type LifeCycleFn = (props: ParcelProps & CustomProps) => Promise<any>;
398
```