When a parent component and its child component both re-render (e.g., both have subscriptions that fire simultaneously), event delegation permanently breaks. Events stop firing because the VTree's VComp.child reference points to a stale child VTree that doesn't match the actual DOM.
- Create a parent component with a subscription (e.g.,
M.subs = [someSubscription]) - Nest a child component that also has its own subscription
- Trigger both subscriptions simultaneously (e.g., via WebSocket message)
- Click on elements rendered by the child component
- Result: Click events don't fire; event handlers are never found
The bug is timing-dependent. If the child happens to render before the parent, it works. If the parent renders first (more common), events break permanently until page reload.
The issue is in ts/miso/dom.ts (compiled to js/miso.js) in the VComp diff logic:
// ts/miso/dom.ts lines 11-17
else if (c.type === VTreeType.VComp && n.type === VTreeType.VComp) {
if (n.key === c.key) {
n.child = c.child; // New VComp gets reference to old child VTree
return; // But old child's .parent still points to OLD VComp!
}
replace(c, n, parent, context);
}And in ts/miso/util.ts:
// ts/miso/util.ts lines 223-231
export function updateRef <T> (current: VTree<T> , latest: VTree<T>) : void {
if (!(current.parent)) {
return;
}
latest.nextSibling = current.nextSibling ? null : current.nextSibling;
latest.parent = current.parent;
(current.parent as VComp<T>).child = latest; // Updates current.parent's child
}When both parent and child have subscriptions and both fire:
-
Parent renders first:
- Creates NEW VComp (let's call it
VComp-B) for the child component slot diff()runs:VComp-B.child = VComp-A.child(copies old child VTree reference)- Old child VTree's
.parentstill points toVComp-A - Parent stores new VTree (containing
VComp-B) in its IORef
- Creates NEW VComp (let's call it
-
Child renders second:
- Creates new child VTree with updated content and fresh domRefs
- Calls
updateRef(oldChildVTree, newChildVTree) updateRefdoes:oldChildVTree.parent.child = newChildVTree- But
oldChildVTree.parentisVComp-A(the OLD VComp)! VComp-A.childgets updated, butVComp-B.childstill points to stale VTree
-
Event delegation:
- Walks the root VTree from parent's IORef
- Finds
VComp-B(the new VComp) - Follows
VComp-B.child→ gets the OLD child VTree with stale/empty children - Cannot match DOM elements → events silently fail
BEFORE parent re-render:
VComp-A.child ──→ ChildVTree-1 (parent: VComp-A)
↓
[DOM elements]
AFTER parent re-render (BUG):
VComp-A.child ──→ ChildVTree-1 (parent: VComp-A) ← orphaned, but child updates this!
VComp-B.child ──→ ChildVTree-1 (parent: VComp-A) ← event delegation uses this!
↓
[stale DOM refs]
Actual DOM has new elements that don't match ChildVTree-1
AFTER child's updateRef:
VComp-A.child ──→ ChildVTree-2 (correct, but VComp-A is orphaned)
VComp-B.child ──→ ChildVTree-1 (still stale!) ← event delegation still fails!
Update the child's parent reference when copying during VComp diff:
// ts/miso/dom.ts
else if (c.type === VTreeType.VComp && n.type === VTreeType.VComp) {
if (n.key === c.key) {
n.child = c.child;
// FIX: Update child's parent to point to NEW VComp
if (c.child) {
c.child.parent = n;
}
return;
}
replace(c, n, parent, context);
}With the fix:
n.child = c.child→ New VComp gets old child VTree (same as before)c.child.parent = n→ Old child's parent now points to NEW VComp- When child's
updateRefruns:current.parent.child = latest current.parentis nowVComp-B(the new VComp)VComp-B.childgets the updated child VTree- Event delegation traverses
VComp-B→ finds correct child → events work!
AFTER fix:
VComp-B.child ──→ ChildVTree-1 (parent: VComp-B) ← parent ref updated!
↓
[DOM elements]
AFTER child's updateRef (with fix):
VComp-B.child ──→ ChildVTree-2 (parent: VComp-B) ← correctly updated!
↓
[new DOM elements match!]
Console logs from debugging session showing the issue:
[MISO] VComp diff: copying child ref {
key: "child-component",
oldVCompId: undefined, // Old VComp
newVCompId: 1, // New VComp created
oldChildParentId: undefined
}
[MISO] updateRef called {
currentParentKey: "child-component",
currentParentId: undefined // Updating OLD VComp, not new one!
}
[MISO] Event delegation: traversing VComp {
key: "child-component",
vcompId: 1, // Traversing NEW VComp
childParentId: undefined // But child's parent is OLD VComp!
}
[MISO] VNode children mismatch! {
numChildren: 0, // VTree has no children
stackTopInDocument: true // But clicked element exists in DOM!
}
- Affects any application with nested components where parent and child both have subscriptions
- Events permanently stop working after the race condition triggers
- Only workaround before fix: ensure only leaf components have subscriptions, or reload page
ts/miso/dom.ts(orjs/miso.js): Addc.child.parent = naftern.child = c.childin VComp diff