Under the hood
Static reactivity
How declared source dependencies determine which updaters run after a state change.
The compiler identifies which values each updater reads and writes. Runtime helpers declare their dependencies through the same source masks. Vidact uses these declarations to schedule work without observing property reads while your code executes.
Sources and updaters
Every compiled component is a set of sources and a set of updaters.
A source is a value that can change after mount: a useState slot, a prop, a context value, an external-store snapshot. Each source gets a numeric ID at compile time.
An updater is a unit of work with a static list of the sources it reads and the sources it writes. Kinds of updater include:
- Derivation: recompute a local value like
const total = price * quantity. Writes a derived source. - DOM binding: set a text node, attribute, property, style, or class. Writes the DOM.
- Range: switch a conditional branch or reconcile a keyed list. Writes the DOM and may construct or dispose owners.
- Prop: pass a new value to a child component. Writes the child's prop source.
- Effect: schedule a
useEffectoruseLayoutEffectcallback for the commit phase.
The compiler topologically orders the updaters it emits and rejects cycles or multiple writers in that graph. Runtime helpers can register additional updaters during construction. The runtime combines their declared dependencies with the compiler's graph, placing producers before consumers and caching the resulting order until registrations change.
Masks
Reads and writes are stored as bit masks over source IDs. When a source changes, the runtime adds its bit to a dirty mask. During a flush it checks updaters in the cached order and runs those whose read masks intersect the dirty mask. Derived writes can make downstream updaters eligible in the same pass. Further invalidations can require another pass, bounded by a stabilization limit.
Masks are not limited to 32 sources; the compiler widens them as needed.
A write, step by step
const [query, setQuery] = useState('')const results = search(items, query)const count = results.length<input value={query} onChange={(e) => setQuery(e.target.value)} /><p>{count} results</p><ul>{results.map((r) => <li key={r.id}>{r.title}</li>)}</ul>- The
inputevent fires and the compiled handler opens a batch. setQuerystores the new string and marks sourcequerydirty.- The handler returns and the batch flushes.
- Updater "recompute
results" readsquery: runs, marksresultsdirty. - Updater "recompute
count" readsresults: runs, markscountdirty. - Updater "set input value" reads
query: runs. - Updater "set text
count" readscount: runs. - Updater "reconcile list" reads
results: runs, moving, inserting, and removing<li>rows by key. - Effects scheduled during the flush run in their commit phase.
Batching
Writes inside a compiled event handler are collected and flushed when the handler returns. Writes from elsewhere, such as after an await, are flushed on the next microtask. Multiple dirty sources can be handled by one updater execution per pass; follow-up writes may schedule another pass.
The synchronous core has exactly one priority. Transitions and deferred values, when enabled through the concurrent feature, add an interruptible lane on top of it.
Derived values versus effects
The compiler turns const doubled = count * 2 into a derived updater when needed. useMemo is not required to make it reactive, though memoization hooks still preserve observable identity. Effects run in their commit phases and handle work such as subscriptions, timers, and browser APIs.
Declared dependencies
Vidact can schedule an updater only from its declared reads and writes. It cannot infer a missing dependency by inspecting the callback at runtime. This keeps property reads free of tracking hooks, but requires code the compiler can analyze and accurate masks from runtime helpers.
Update cycles
Cycles in runtime-composed graphs use stable registration order. Effects and subscriptions can also cause repeated writes. If the work does not stabilize within the runtime's pass limit, Vidact throws an error. Check for a callback that repeatedly writes a value it depends on.