Skip to content

Under the hood

Ownership and identity

Who owns each piece of DOM, when it is created and disposed, and what survives an update.

Without a virtual tree to diff, Vidact needs a different answer to the question "what happens to this DOM when things change?" The answer is ownership: every dynamic region of the page has exactly one owner, and the owner's identity decides whether the region is kept, updated, or thrown away.

Owners

An owner is a runtime record that holds DOM nodes, bindings, effects, refs, subscriptions, and child owners. When an owner is disposed, everything it holds is cleaned up: effect cleanups run, refs are set to null, subscriptions are cancelled, child owners are disposed recursively, and the DOM nodes are removed.

The owners in a Vidact application are:

OwnerCreated whenDisposed when
RootmountCompiled or hydrateRoot is calleddispose() or unmount()
ComponentA parent constructs itThe parent's range or list no longer includes it
Conditional branchA condition selects itThe condition selects a different branch
List recordA key appears in a rendered arrayThe key disappears
Error boundary contentThe boundary mounts or reset is calledThe content throws
PortalThe portal expression is constructedIts logical parent is disposed

Owners nest by construction, not by DOM position. A component rendered into a portal is owned by the component that created the portal, even though its nodes live elsewhere in the document. Context, error boundaries, and cleanup all follow this logical tree.

Identity

Identity is what lets an owner survive an update. Each kind of owner has a source of identity:

OwnerIdentity
ComponentThe construction site in the parent (this <Child /> at this position)
Conditional branchWhich branch of the expression is selected
Keyed list recordThe key
Unkeyed list recordThe index

An update that keeps the identity keeps the owner: its DOM, its state, its focus, its scroll position. An update that changes the identity disposes the old owner and constructs a new one.

Concretely:

  • Changing a prop on <Child value={x} /> keeps Child; its prop updater runs.
  • Switching cond ? <A /> : <B /> from true to false disposes A and constructs B.
  • Switching cond ? <A x="1" /> : <A x="2" /> also disposes and constructs, because the branches are different owners even though the component is the same.
  • Reordering a keyed list moves owners; it disposes none of them.
  • Reordering an unkeyed list keeps owners by position and updates their props, so a row's state may end up attached to a different item.

These are the same rules React applies during reconciliation. Vidact reaches them from the compiled ownership tree instead.

Ranges

A component or branch may own several top-level nodes, or none. Vidact tracks each such region as a range delimited by lightweight markers, so it can insert, move, and remove the region as a unit without wrapping it in an extra element. Server output includes the markers as comments, which is how hydration finds the ranges again.

Disposal order and safety

Disposal is depth-first: children are disposed before their parent's own cleanup runs, so a parent's effect cleanup can still see child DOM if it needs to. Writing state that belongs to a disposed owner is an error ("cannot update state after disposal" in development, V012 in production). A late setState from a resolved promise or a leftover timer means a cleanup is missing, so Vidact reports it instead of silently dropping the write; cancel timers and subscriptions in your effect cleanups.

When a batch fails partway through, the owners it created are disposed and the DOM writes it made are rolled back before the error is routed to a boundary. The page is never left with half of an update applied.

Roots

A root is the owner with no parent. It records the error callbacks and useId prefix for everything below it. createRoot(...).replace(nextApp) constructs the next application's owner first and disposes the previous one after, so the host element is never briefly empty. Hot module replacement and Vidact Start navigation both use this mechanism.

What this means for your code

  • Put state where its lifetime should be. If a value should survive a branch switch, hoist it above the conditional.
  • Use stable keys for lists whose rows have state or focus.
  • Assume cleanup is reliable. An effect cleanup, a callback-ref cleanup, and a store unsubscribe all run exactly once when their owner goes away, including during error recovery.