Skip to content

Instantly share code, notes, and snippets.

@marmay
Created January 3, 2026 13:17
Show Gist options
  • Select an option

  • Save marmay/bd2f582badb76c1a1806aa3f6e206dcd to your computer and use it in GitHub Desktop.

Select an option

Save marmay/bd2f582badb76c1a1806aa3f6e206dcd to your computer and use it in GitHub Desktop.

Bug Report: VComp Child Parent Reference Not Updated During Diff

Summary

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.

Reproduction Scenario

  1. Create a parent component with a subscription (e.g., M.subs = [someSubscription])
  2. Nest a child component that also has its own subscription
  3. Trigger both subscriptions simultaneously (e.g., via WebSocket message)
  4. Click on elements rendered by the child component
  5. 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.

Root Cause

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
}

The Race Condition

When both parent and child have subscriptions and both fire:

  1. 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 .parent still points to VComp-A
    • Parent stores new VTree (containing VComp-B) in its IORef
  2. Child renders second:

    • Creates new child VTree with updated content and fresh domRefs
    • Calls updateRef(oldChildVTree, newChildVTree)
    • updateRef does: oldChildVTree.parent.child = newChildVTree
    • But oldChildVTree.parent is VComp-A (the OLD VComp)!
    • VComp-A.child gets updated, but VComp-B.child still points to stale VTree
  3. 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

Visual Diagram

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!

The Fix

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);
}

Why This Works

With the fix:

  1. n.child = c.child → New VComp gets old child VTree (same as before)
  2. c.child.parent = nOld child's parent now points to NEW VComp
  3. When child's updateRef runs: current.parent.child = latest
  4. current.parent is now VComp-B (the new VComp)
  5. VComp-B.child gets the updated child VTree
  6. 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!]

Diagnostic Evidence

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!
}

Impact

  • 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

Files Changed

  • ts/miso/dom.ts (or js/miso.js): Add c.child.parent = n after n.child = c.child in VComp diff
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment