Skip to content

UI component lifecycle

A Matrix UI component goes through six steps from document.createElement to fully mounted: construct → connect → find parent → receive context → bootstrap → ready. This page covers each step and the hooks you can override.

1. Construct

The browser calls your constructor when the element is created (whether by document.createElement('my-tag') or by HTML parsing). The base constructor (MatrixActorHtmlElement.ts:361-367) does two things:

ts
constructor() {
  super();
  this.attachShadow({ mode: 'open' });
  this.ready = new Promise<void>(resolve => { this._resolveReady = resolve; });
}

A shadow root is attached. The ready promise is allocated. Nothing else happens. You should not override the constructor for setup work — there is no context yet, so no actor, no bus, no template. Use onBootstrap() (which fires after context is received).

2. connectedCallback

Called when the element is inserted into a DOM tree (MatrixActorHtmlElement.ts:535-568). The framework:

  1. Marks this._connected = true.
  2. If this element previously disconnected and reconnected within the same microtask (a DOM move), the existing _actor/_context are still alive — re-arm fact subscriptions and bail.
  3. Otherwise look for a parent shell via _findParentShell().
  4. If a parent shell exists, register as a pending child (parentShell._registerPendingChild(this)).
  5. If no parent shell but inside a MatrixDSLHost, do nothing — the host bootstraps children explicitly.
  6. Otherwise start orphan detection: warn after static contextTimeoutMs (default 5000ms) if no context has arrived.

You do not override connectedCallback directly. The base implementation handles the framework wiring; your custom setup goes in _onContextUpdated() or onBootstrap().

3. Parent finds child → context cascades down

The parent shell's _registerPendingChild(child) (MatrixActorHtmlElement.ts:853-865) decides whether the child gets context immediately:

  • If the parent has its context, derive a child context (parent.context.derive(childName)) and call child._receiveContext(...).
  • If the parent does not have context yet, queue the child in _pendingChildren. When the parent later receives its context, it processes the queue.

Subscriptions to the child's $events are registered before giving the child context (the "subscribe-before-context invariant"). Without this, fast-emitting children could fire events before the parent had a chance to listen.

4. _receiveContext — the heavy lifting

_receiveContext(context, existingActor?) (MatrixActorHtmlElement.ts:637-832) is the framework-internal entry point that wires everything up. The first non-trivial call:

  1. Records the context.
  2. Creates the internal actor (_createActor()) — Pattern A DynamicActor or Pattern B's actor class.
  3. Injects shell info (_shellInfo, _shellElement, _shellUpdateCallback).
  4. Wires delegated DOM events (_wireDelegatedEvents()).
  5. Bridges initial element attributes onto the actor.
  6. Renders the template into the shadow DOM (_renderTemplate()).
  7. Seeds static state initial values.
  8. Schedules the first reactive render if render(state) is defined.
  9. Fires off non-blocking KV hydration.
  10. Initializes the actor (actor.initialize(context, name) — see Actor lifecycle).
  11. Performs the $join handshake with the parent (local fast-path if same JS context; NATS round-trip otherwise).
  12. Wires shell-level accepts handlers (Pattern A) and tracking (Pattern B).
  13. Recursively wires children.
  14. Calls _onContextUpdated(context) — your override hook.
  15. Starts fact-plane hydration.
  16. Calls onBootstrap() — your override hook.
  17. Resolves ready and sets data-matrix-ready on the element.

Tip: Two override points, in order:

  • _onContextUpdated(context) — runs after template + actor + handshake but before fact hydration and onBootstrap. Override when you need to wire something that depends on having context but that runs every remount.
  • onBootstrap() — runs once. Use it for one-time setup that needs the actor on the bus.

5. The $join / $join_ack handshake

When the actor is initialized, the element publishes $join to the parent's $inbox. The parent's actor (on$join, MatrixActor.ts:704-745) registers the child and replies with $join_ack.

The framework picks the fastest path:

  • Local fast-path (MatrixActorHtmlElement.ts:761-764): if the parent shell is in the same browser tab, directly call parentShell._actor.on$join(payload) — no NATS round-trip.
  • Remote path (:765-792): publish $join on the parent's $inbox topic, subscribe to own $inbox, await $join_ack with a 5s timeout.

A timeout on the remote path throws and aborts _receiveContext. Your component's ready rejects, and the error surfaces to whatever attempted the mount.

6. onMounted for code-generated components

MatrixActorHtmlElement.ts:2562-2566 calls a separate onMounted() hook for components built dynamically via the factory path (factory.create-from-code, component.save). This is specifically for LLM-written code — those components don't have access to the protected lifecycle hooks because they are compiled from a string.

Static TypeScript components do not use onMounted(). Use _onContextUpdated() or onBootstrap().

disconnectedCallback and microtask move detection

When the element is removed from the DOM (MatrixActorHtmlElement.ts:570-606):

  1. Mark _connected = false.
  2. Clear orphan timeout.
  3. Clear fact subscriptions.
  4. Defer disposal to a microtask. If the element reconnects within the same microtask (a DOM move), bail and keep the actor alive. Only if the element is still disconnected after the microtask do we send $leave to the parent and dispose the actor.

This matches LIT/Polymer's "stable identity across moves" invariant. It means dragging an element across the DOM tree does not destroy and recreate its actor.

attributeChangedCallback

When an observed attribute changes (MatrixActorHtmlElement.ts:608-627):

  1. Compute the handler name: on{toHandlerCase(name)}Changed.
  2. Try the actor first; then try the element class itself.
  3. Call the handler with (newValue, oldValue).

Declare what to observe via static observedAttributes on the actor class (Pattern B) or the element class (Pattern A).

Lifecycle vs actor lifecycle

The element lifecycle is browser-driven (DOM connection). The actor lifecycle is framework-driven (initialize / dispose). They are coupled:

Element eventActor event
connectedCallback(no immediate actor effect — waiting for context)
_receiveContext for the first timeactor.initialize(context, name)
_receiveContext again with new contextactor.initialize(newContext, name) (remount path)
disconnectedCallback (after microtask, no move)actor.dispose()

Common gotchas

  • setState in constructor → throws. No actor yet. Move to onBootstrap().
  • Touching this.shadowRoot in constructor. Allowed — shadow root is attached. But the template is not rendered yet. Touch the rendered DOM in _onContextUpdated().
  • DOM access from a custom element with no context. The element has been constructed but not given context. Reads work; writes that touch the actor (setState, emit) fail. Always gate on await this.ready before driving the element from outside.
  • Parent shell re-attached without context. If the parent's actor died and the element was reused, you'll see a stale _contextReceived flag get cleared (MatrixActorHtmlElement.ts:670-674). The framework does the right thing; do not try to reset state manually.

See also

Source: projects/matrix-3/packages/core/src/framework/MatrixActorHtmlElement.ts:361-367 (constructor), :535-606 (connectedCallback/disconnectedCallback), :608-627 (attributeChangedCallback), :637-832 (_receiveContext), :853-865 (_registerPendingChild).