demo · v131

Component Lifecycle

A custom element that keeps an AsyncDisposableStack alive for the whole DOM lifetime: connectedCallback() registers an IntersectionObserver, a ResizeObserver, and an interval timer, while disconnectedCallback() calls disposeAsync() so none of those resources keep running after removal.

Checking AsyncDisposableStack support…

live components

No components mounted.

Active components: 0 · Active resources: 0 · Disposed: 0
lifecycle log
waiting…

the code

The key detail is that the stack survives until the element leaves the DOM. Do not wrap connectedCallback() in await using; that lexical scope exits immediately after setup.

class LiveCard extends HTMLElement {
  #resources = null;

  async connectedCallback() {
    if (this.#resources) return;

    const stack = new AsyncDisposableStack();

    const io = new IntersectionObserver((entries) => this.#onIntersect(entries));
    io.observe(this);
    stack.defer(async () => io.disconnect());

    const ro = new ResizeObserver(() => this.#onResize());
    ro.observe(this);
    stack.defer(async () => ro.disconnect());

    const id = setInterval(() => this.#onTick(), 1000);
    stack.defer(async () => clearInterval(id));

    this.#resources = stack;
  }

  async disconnectedCallback() {
    const stack = this.#resources;
    this.#resources = null;
    await stack?.disposeAsync();
  }
}

see also