fix: preserve Vue node reactivity during undo/redo operations (#7222)

## Summary
preserve Vue node reactivity during undo/redo operations

Root Cause: The Vue reactivity chain was broken during undo/redo
operations:
1. handleDeleteNode was deleting nodeRefs and nodeTriggers
2. Vue components still held references to the old refs
3. When nodes were recreated, finalizeOperation tried to call triggers
but they were already deleted
4. Vue didn't know the data had changed, so nodes didn't visually update

fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/7040

## Screenshots


https://github.com/user-attachments/assets/2feb294a-36e8-4bbe-b3f7-b7015066abc5

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7222-fix-preserve-Vue-node-reactivity-during-undo-redo-operations-2c36d73d3650819ab72afb10cbdaf39a)
by [Unito](https://www.unito.io)
This commit is contained in:
Terry Jia
2025-12-08 13:38:27 -05:00
committed by GitHub
parent 8c5584c997
commit 6820633fea
2 changed files with 26 additions and 5 deletions

View File

@@ -956,6 +956,15 @@ class LayoutStoreImpl implements LayoutStore {
return this.currentActor
}
/**
* Clean up refs and triggers for a node when its Vue component unmounts.
* This should be called from the component's onUnmounted hook.
*/
cleanupNodeRef(nodeId: NodeId): void {
this.nodeRefs.delete(nodeId)
this.nodeTriggers.delete(nodeId)
}
/**
* Initialize store with existing nodes
*/
@@ -964,8 +973,10 @@ class LayoutStoreImpl implements LayoutStore {
): void {
this.ydoc.transact(() => {
this.ynodes.clear()
this.nodeRefs.clear()
this.nodeTriggers.clear()
// Note: We intentionally do NOT clear nodeRefs and nodeTriggers here.
// Vue components may already hold references to these refs, and clearing
// them would break the reactivity chain. The refs will be reused when
// nodes are recreated, and stale refs will be cleaned up over time.
this.spatialIndex.clear()
this.linkSegmentSpatialIndex.clear()
this.slotSpatialIndex.clear()
@@ -995,6 +1006,9 @@ class LayoutStoreImpl implements LayoutStore {
// Add to spatial index
this.spatialIndex.insert(layout.id, layout.bounds)
})
// Trigger all existing refs to notify Vue of the new data
this.nodeTriggers.forEach((trigger) => trigger())
}, 'initialization')
}
@@ -1085,8 +1099,10 @@ class LayoutStoreImpl implements LayoutStore {
if (!this.ynodes.has(operation.nodeId)) return
this.ynodes.delete(operation.nodeId)
this.nodeRefs.delete(operation.nodeId)
this.nodeTriggers.delete(operation.nodeId)
// Note: We intentionally do NOT delete nodeRefs and nodeTriggers here.
// During undo/redo, Vue components may still hold references to the old ref.
// If we delete the trigger, Vue won't be notified when the node is re-created.
// The trigger will be called in finalizeOperation to notify Vue of the change.
// Remove from spatial index
this.spatialIndex.remove(operation.nodeId)

View File

@@ -1,4 +1,4 @@
import { computed, toValue } from 'vue'
import { computed, onUnmounted, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
@@ -17,6 +17,11 @@ export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter<string>) {
// Get the customRef for this node (shared write access)
const layoutRef = layoutStore.getNodeLayoutRef(nodeId)
// Clean up refs and triggers when Vue component unmounts
onUnmounted(() => {
layoutStore.cleanupNodeRef(nodeId)
})
// Computed properties for easy access
const position = computed(() => {
const layout = layoutRef.value