Appearance
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:
- Marks
this._connected = true. - If this element previously disconnected and reconnected within the same microtask (a DOM move), the existing
_actor/_contextare still alive — re-arm fact subscriptions and bail. - Otherwise look for a parent shell via
_findParentShell(). - If a parent shell exists, register as a pending child (
parentShell._registerPendingChild(this)). - If no parent shell but inside a
MatrixDSLHost, do nothing — the host bootstraps children explicitly. - 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 callchild._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:
- Records the context.
- Creates the internal actor (
_createActor()) — Pattern ADynamicActoror Pattern B's actor class. - Injects shell info (
_shellInfo,_shellElement,_shellUpdateCallback). - Wires delegated DOM events (
_wireDelegatedEvents()). - Bridges initial element attributes onto the actor.
- Renders the template into the shadow DOM (
_renderTemplate()). - Seeds
static stateinitial values. - Schedules the first reactive render if
render(state)is defined. - Fires off non-blocking KV hydration.
- Initializes the actor (
actor.initialize(context, name)— see Actor lifecycle). - Performs the
$joinhandshake with the parent (local fast-path if same JS context; NATS round-trip otherwise). - Wires shell-level
acceptshandlers (Pattern A) and tracking (Pattern B). - Recursively wires children.
- Calls
_onContextUpdated(context)— your override hook. - Starts fact-plane hydration.
- Calls
onBootstrap()— your override hook. - Resolves
readyand setsdata-matrix-readyon the element.
Tip: Two override points, in order:
_onContextUpdated(context)— runs after template + actor + handshake but before fact hydration andonBootstrap. 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 callparentShell._actor.on$join(payload)— no NATS round-trip. - Remote path (
:765-792): publish$joinon the parent's$inboxtopic, subscribe to own$inbox, await$join_ackwith 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):
- Mark
_connected = false. - Clear orphan timeout.
- Clear fact subscriptions.
- 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
$leaveto 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):
- Compute the handler name:
on{toHandlerCase(name)}Changed. - Try the actor first; then try the element class itself.
- 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 event | Actor event |
|---|---|
connectedCallback | (no immediate actor effect — waiting for context) |
_receiveContext for the first time | actor.initialize(context, name) |
_receiveContext again with new context | actor.initialize(newContext, name) (remount path) |
disconnectedCallback (after microtask, no move) | actor.dispose() |
Common gotchas
setStatein constructor → throws. No actor yet. Move toonBootstrap().- Touching
this.shadowRootin 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 onawait this.readybefore 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
_contextReceivedflag get cleared (MatrixActorHtmlElement.ts:670-674). The framework does the right thing; do not try to reset state manually.
See also
- MatrixActorHtmlElement — the static + instance surface.
- Actor lifecycle — the headless half.
- Shadow DOM and rendering — what
_renderTemplatedoes.
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).