Overview
The reconciler implements keyed child diffing to minimize native widget churn when updating JSX trees. It reuses existing instances when keys match, unmounts removed children, and fixes native child ordering with minimal FFI calls.Key Concepts
VNode (Virtual Node)
A lightweight descriptor produced by the JSX factory:- type:
"Box","Text", etc. for intrinsic elements;Fragmentfor child-only grouping; function for components - props: Props object (excluding
childrenandkey) - key: Optional identifier for reconciliation (defaults to positional index if
null) - children: Normalized child VNodes (always an array)
Instance (Mounted State)
Bookkeeping for a mounted VNode:- widget: The native
Widgetinstance (ornullfor Fragments) - cleanups: Functions to dispose signal effects (called during unmount)
- eventHandlers: JSX event callbacks (
onClick,onSubmit, etc.)
reconcileChildren()
Reconcile old children against new VNodes using keyed diffing.The parent instance whose children are being reconciled.
The new array of child VNodes.
void (mutates parentInstance.children in place)
Algorithm
- Build key map: Create
Map<key, Instance>from old children (key defaults to index ifnull) - Walk new VNodes: For each new VNode:
- If a keyed match exists: reuse instance, update props, remove from map
- If no match: mount new instance, append to parent widget
- Unmount remaining: Destroy instances left in the map (removed children)
- Fix ordering: Use
insertChild()to reorder native children to match new order
Keyed vs. Positional Reconciliation
Without Keys (Positional Matching)
Children are matched by index:With Keys (Identity Matching)
Children are matched by key:Use keys when:
- Reordering items (drag-and-drop, sorting)
- Adding/removing items from lists
- Items have stable identifiers (IDs, UUIDs)
- List order is always static
- Items are truly positional (e.g., “first 10 results”)
Fragment Reconciliation
Fragments have no native widget — children are mounted into the nearest widget-bearing ancestor.Fragment Update Logic
When a Fragment updates:- Build key map from old children
- Walk new VNodes, reusing keyed matches or mounting new instances
- Find ancestor widget for append/insert operations (Fragments themselves have no widget)
- Unmount removed children
Component Function Updates
When a component function’s props change:- Re-invoke the function with new props
- Resolve nested component functions until an intrinsic element or Fragment is reached
- Dispose old signal effects
- Re-apply props from the resolved VNode to the existing widget
- Reconcile children
Component functions do not have lifecycle hooks. Use
effect() for side effects:Prop Update Lifecycle
When an instance updates:- Dispose old effects: Call all cleanup functions in
instance.cleanups - Clear event handlers: Remove from global event registry
- Re-apply props: Parse and apply all props via FFI
- Static props: applied once
- Signal props: wrapped in new
effect(), cleanup added toinstance.cleanups
- Reconcile children: Call
reconcileChildren()if children changed
Static vs. Signal Props
Ordering Algorithm
After reconciliation, native child order may not match new VNode order. The reconciler fixes this with minimal FFI calls:- tui_get_child_at: Query current child at index
i - insertChild: Move child to index
i(detaches from old position, inserts at new position) - Only called when position is wrong
Event Handler Registry
JSX event handlers (onClick, onSubmit, etc.) are stored in a global registry:
- Key: Widget handle
- Value: Map of event name → handler function
getEventHandlers(handle) to dispatch events:
Performance Characteristics
Time Complexity
- Keyed reconciliation: O(n) where n = max(old children, new children)
- Ordering fix: O(n) with O(moves) FFI calls where moves = number of children in wrong position
- Unmount: O(subtree size) for effects, O(1) FFI per widget-bearing node (via
destroySubtree)
Space Complexity
- Key map: O(old children)
- Instance tree: O(total mounted nodes)
FFI Call Budget
For reconciling n children:- Best case: 0 FFI calls (no changes)
- Worst case: O(n) insert calls + O(removed) destroy calls
- Typical case: O(added + removed + moves) calls
Limitations
No Diffing Within Props
Props are re-applied wholesale on update. There is no granular prop diffing:No Async Components
Component functions must return a VNode synchronously. Noasync/await support.
No Refs to Component Instances
Component functions have no instance object. Use refs on child widgets:See Also
- render.mdx — Mounting and unmounting JSX trees
- signals.mdx — Signal-driven reactivity
- createLoop() — Event loop with JSX event dispatch