Frontmatter
| id | 8868 |
| title | Fix: syncVnodeTree should nullify vnode for removed children |
| state | Closed |
| labels | bugaicore |
| assignees | tobiu |
| createdAt | Jan 23, 2026, 8:58 PM |
| updatedAt | Jan 23, 2026, 11:50 PM |
| githubUrl | https://github.com/neomjs/neo/issues/8868 |
| author | tobiu |
| commentsCount | 2 |
| parentIssue | null |
| subIssues | [] |
| subIssuesCompleted | 0 |
| subIssuesTotal | 0 |
| blockedBy | [] |
| blocking | [] |
| closedAt | Jan 23, 2026, 11:50 PM |
Fix: syncVnodeTree should nullify vnode for removed children

tobiu
Jan 23, 2026, 11:04 PM
Input from Gemini 3 Pro:
✦ I attempted to implement the fix for this issue but encountered regressions that caused VDOM update cycles to time out in tests.
Learnings & Analysis:
- Logical Child Iteration: Switching from
getChildren(vnode)toComponentManager.find({parentId})is correct for identifying all managed children, including those that might have been removed.- Placeholder Complexity: My attempt to handle "placeholders" (nodes with
componentId) insidesyncVnodeTreewas flawed. The logic to distinguish between a full VNode update and a placeholder reference is subtle.createVdomReferencegenerates an object withcomponentId, butaddVnodeComponentReferencestransforms the tree. The interaction between these methods andsyncVnodeTreeneeds careful tracing.- Update Cycle Deadlocks: The changes caused
promiseUpdate()to hang (timeout) in multiple tests. This strongly suggests that by modifying howvnodeInitializedormountedwas set (or skipped), I inadvertently broke the resolution chain forVDomUpdate, leaving updates in an "in-flight" state.- The "Silent" Update: The
_vnodeupdate insyncVnodeTreeis critical. If we skip it (as I tried for placeholders), we might miss necessary state synchronization that the framework expects, leading to the deadlocks.Recommendation for Next Attempt:
- Focus strictly on the missing children case first.
- Debug
syncVnodeTreeto log exactly what the VDom Worker sends back for a "placeholder" vs. a "full" node.- Ensure that any logic modifying
mountedor_vnodedoes not interfere withVDomUpdate.executeCallbacks.

tobiu
Jan 23, 2026, 11:50 PM
Input from Gemini 3 Pro:
✦ I have implemented the fix for this issue.
Changes:
Modified
src/mixin/VdomLifecycle.mjsto use a Hybrid Two-Pass Strategy:
- Pass 1 (Update): Iterates over
visibleChildren(found in the new VNode structure). This matches the original behavior and preserves the full VNode structure for placeholders, preventing regressions inTreeBuilder/Helperlogic (which relies on symmetry).- Pass 2 (Unmount): Iterates over ALL logical children (
ComponentManager.find). If a child is NOT visible AND not found as a placeholder (checked viaVNodeUtil.find), it is correctly unmounted (_vnode = null,mounted = false).Added a regression test
test/playwright/unit/vdom/HiddenChildren.spec.mjsthat reproduces the issue by hiding a child (removeDom: true) inside a container and verifying it unmounts correctly.Verification:
HiddenChildren.spec.mjspasses (Child is unmounted).RealWorldUpdates.spec.mjspasses (No extra deltas/regressions).- Full
npm run test-unitpasses (250 tests).The fix ensures correct state synchronization without disrupting the delicate balance of VDOM merging and placeholder handling.
tobiu assigned to @tobiu on Jan 23, 2026, 11:50 PM
tobiu closed this issue on Jan 23, 2026, 11:50 PM
tobiu cross-referenced by #8872 on Jan 24, 2026, 12:21 AM
tobiu cross-referenced by #8874 on Jan 24, 2026, 12:30 AM
Description: The
syncVnodeTreemethod insrc/mixin/VdomLifecycle.mjsupdates component state based on the new VNode structure returned by the VDom Worker. Currently, it iterates only over the new VNode's children. If a child component was removed (e.g.removeDom: true), it is skipped bysyncVnodeTree, leaving the child with a stale_vnodereference andmounted: true.Goal: Ensure
component._vnodeisnullandmountedisfalsewhen a component is removed from the DOM. This aligns the App Worker state with the actual DOM state.Proposed Change:
syncVnodeTreeto iterate Logical Children (ComponentManager.find({parentId: me.id}))._vnode = nullandmounted = false.Verification: This change will likely break
test/playwright/unit/vdom/VdomLifecycle.spec.mjs, specifically the test "vnode should PERSIST when component is hidden". This test should be updated to reflect the new, correct behavior (vnode should be null).Related Files:
src/mixin/VdomLifecycle.mjs