Dismissal blockers
Gate user-intent dismissal with addBlocker, dismissing, dismissAll modes, and force.
Blockers let you ask “discard unsaved changes?” before a layer closes. They gate engine dismissal paths — call.end/call.dismiss and stack.dismiss/stack.dismissAll. Backdrop clicks and Esc are not core events; wire them to call.end/call.dismiss if they should consult blockers. System teardown is never gated.
See Blockers for the full model.
Register blockers
Two scopes, one gate — any veto blocks the attempt:
// instance scope — dirty state on this layer
const dispose = call.addBlocker(() => {
if (isDirty) return false; // veto
return true; // allow
});
// stack scope — app-wide policy
const disposeStack = stack.addBlocker((layer) => {
return !layer.payload.locked;
});
Both return a disposer. Predicates may be async; dismiss awaits them.
Return value and force
call.end and call.dismiss return Promise<boolean> — true if dismissed, false if vetoed:
const didDismiss = await call.dismiss(); // void-R: omit the response
if (!didDismiss) {
// show confirm UI — your own layer, not opened by core
}
Bypass blockers with { force: true }:
await call.end(response, { force: true });
await call.dismiss(response, { force: true });
// void-R: await call.end(undefined, { force: true });
While blockers evaluate, dismissing is true on the layer state — disable close buttons during async confirm:
function Editor({ call, dismissing }: LayerComponentProps<void, void>) {
return (
<button disabled={dismissing} onClick={() => void call.dismiss()}>
Close
</button>
);
}
dismissAll modes
stack.dismissAll(...args) is async (omit response for void-R):
| Mode | Behavior |
|---|---|
"skipBlocked" (default) |
Attempt each; close permitted, leave blocked open |
"stopAtBlocked" |
Process in order; halt at first blocked |
"force" |
Bypass all blockers |
await stack.dismissAll(undefined, { mode: "stopAtBlocked" });
// void-R + default mode: await stack.dismissAll();
Default mode is configurable via StackOptions.dismissAllMode or LayerClientOptions.defaultStackOptions.
What blockers skip
| Path | Blockers |
|---|---|
call.end / call.dismiss |
Honored |
stack.dismiss / stack.dismissAll |
Honored |
cancelQueued (serial, never mounted) |
Skipped |
cancelAll / component unmount / host disconnect |
Skipped — rejects open() with LayerCancelledError |
| Layer-group cascade on parent dismiss | Skipped — child cancelAll({ reason: "parentDismiss" }); guard the parent |