Skip to content
Vidact

Under the hood

Ownership and identity

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

Owners track the DOM, state, and cleanup work associated with a component or dynamic region. An update either preserves that owner or disposes it, depending on the region's identity.

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 rules depend on Vidact's compiled ownership boundaries. In particular, two branches can own separate component instances even when both use the same component type.

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 runs registered cleanup work and removes owned DOM. Do not depend on child DOM remaining attached during a parent's cleanup. Writing state that belongs to a disposed owner is an error ("cannot update state after disposal" in development, V012 in production). Cancel timers and subscriptions, and ignore late asynchronous results after cleanup.

Vidact stages construction and supported DOM writes before publication. If computation fails, staged owners are disposed; supported publication operations have rollback behavior. This does not undo arbitrary side effects in your callbacks or restore a state value already written. Use error boundaries and test the failure paths your application relies on.

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 before disposing the previous one. Hot module replacement uses root replacement. Vidact Start navigation retains shared route owners and replaces only the changed route suffix.

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.