Lifetime and cancellation¶
Single-page applications create and remove components constantly. A page that is still loading
data when the user navigates away should stop its work, and must certainly not write its results
into a page that no longer exists. JoltUI gives every component a lifetime expressed as a
standard AbortSignal.
The lifetime signal¶
Every CustomElement has a read-only abortSignal. It is aborted when the element is removed from
the DOM (disconnectedCallback). The abort reason is an ElementDisconnectedError.
Pass the signal to anything that accepts one:
@lifecycle(Lifecycle.BEFORE_INIT)
async loadComments(): Promise<void> {
const response = await fetch(`/api/posts/${this.postId}/comments`, {
signal: this.abortSignal,
});
this.comments = await response.json();
}
document.addEventListener("keydown", this.onKey, {
signal: this.abortSignal, // listener removed with the component
});
Work that cannot be cancelled, such as a dynamic import or a library without signal support,
simply finishes. After every such await, check the signal before producing side effects:
async loadDetails(): Promise<void> {
const { renderChart } = await import("./chartLibrary.js");
this.abortSignal.throwIfAborted(); // stop if we were removed meanwhile
renderChart(this.querySelector("#chart"));
}
What JoltUI does on removal¶
When a component is disconnected:
- its
abortSignalis aborted; - its pending initialization promises (
initComplete,initElement()) reject with anElementDisconnectedError; - it stops listening to application data and query-parameter changes;
- event handlers that are still awaiting stop waiting;
- an in-progress render is abandoned before it commits any markup;
AFTER_DISCONNECThooks run, with the signal already aborted.
Cancellation is not an error. ElementDisconnectedError is never passed to the application's
error handlers, even when it propagates through a parent's initialization or through the router.
Successful initialization still resolves initComplete immediately before AFTER_INIT, and once
fulfilled it is not changed by a later disconnection.
Recognising cancellation¶
When you call initElement() or rerender() yourself, or catch errors around work tied to the
signal, use isElementCancellation(error). It also detects a cancellation hidden in an error's
cause chain:
import { isElementCancellation } from "joltui";
try {
await this.rerender();
} catch (error) {
if (isElementCancellation(error)) return; // expected: we were removed
throw error;
}
JoltUI's own event, lifecycle and routing paths already do this.
Overlapping renders¶
If render() or rerender() is called while a previous render is still in progress (for instance
an asynchronous markup() waiting for the network), the latest render wins:
- superseded calls resolve quietly without committing markup or running the remaining hooks;
- existing bindings stay active until the replacement markup is ready;
- the component's lifetime is unaffected.
Synchronous binding refreshes (invalidate, data changes) do nothing once a component's lifetime
has ended.
Combining signals¶
Sometimes work should stop for two reasons: the component disappears, or a newer request
replaces the current one. Search-as-you-type is the classic case. AbortSignal.any combines both:
private searchController: AbortController | null = null;
async search(event: Event): Promise<void> {
this.searchController?.abort();
this.searchController = new AbortController();
const signal = AbortSignal.any([this.abortSignal, this.searchController.signal]);
const query = (event.target as HTMLInputElement).value;
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal });
this.results = await response.json();
} catch (error) {
if (signal.aborted) return; // replaced or removed: not a failure
throw error;
}
}
Navigation and cancellation¶
Every navigation has its own AbortController. When a new navigation starts, the previous one is
aborted and the layouts, sublayouts and page it had started creating are removed, which ends
their lifetimes. Layouts from a navigation that has completed are left alone and can be reused.
Rules for element instances¶
- A removed instance cannot be reused. Its lifetime has ended; create a new element instead.
- Moving an element in the DOM ends its lifetime too, because moving triggers
disconnectedCallback. Do not detach and re-insert components to preserve their state; keep the state in application data instead. - Do not tie shared cached work to one component's lifetime signal (Template caching); cancelling it would affect every waiting component.