diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts
index 619147d55c1..39aece16a5a 100644
--- a/packages/runtime-core/__tests__/hmr.spec.ts
+++ b/packages/runtime-core/__tests__/hmr.spec.ts
@@ -6,6 +6,7 @@ import {
h,
nextTick,
nodeOps,
+ ref,
render,
serializeInner,
triggerEvent,
@@ -415,6 +416,53 @@ describe('hot module replacement', () => {
expect(mountSpy).toHaveBeenCalledTimes(1)
})
+ // #6930
+ test('reload: avoid infinite recursion', async () => {
+ const root = nodeOps.createElement('div')
+ const childId = 'test-child-6930'
+ const unmountSpy = vi.fn()
+ const mountSpy = vi.fn()
+
+ const Child: ComponentOptions = {
+ __hmrId: childId,
+ data() {
+ return { count: 0 }
+ },
+ expose: ['count'],
+ unmounted: unmountSpy,
+ render: compileToFunction(`
{{ count }}
`),
+ }
+ createRecord(childId, Child)
+
+ const Parent: ComponentOptions = {
+ setup() {
+ const com = ref()
+ const changeRef = (value: any) => {
+ com.value = value
+ }
+
+ return () => [h(Child, { ref: changeRef }), com.value?.count]
+ },
+ }
+
+ render(h(Parent), root)
+ await nextTick()
+ expect(serializeInner(root)).toBe(`0
0`)
+
+ reload(childId, {
+ __hmrId: childId,
+ data() {
+ return { count: 1 }
+ },
+ mounted: mountSpy,
+ render: compileToFunction(`{{ count }}
`),
+ })
+ await nextTick()
+ expect(serializeInner(root)).toBe(`1
1`)
+ expect(unmountSpy).toHaveBeenCalledTimes(1)
+ expect(mountSpy).toHaveBeenCalledTimes(1)
+ })
+
// #1156 - static nodes should retain DOM element reference across updates
// when HMR is active
test('static el reference', async () => {
diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts
index b5904a4fc5b..8196eb89195 100644
--- a/packages/runtime-core/src/hmr.ts
+++ b/packages/runtime-core/src/hmr.ts
@@ -139,7 +139,11 @@ function reload(id: string, newComp: HMRComponent) {
// components to be unmounted and re-mounted. Queue the update so that we
// don't end up forcing the same parent to re-render multiple times.
instance.parent.effect.dirty = true
- queueJob(instance.parent.update)
+ queueJob(() => {
+ instance.parent!.update()
+ // #6930 avoid infinite recursion
+ hmrDirtyComponents.delete(oldComp)
+ })
} else if (instance.appContext.reload) {
// root instance mounted via createApp() has a reload method
instance.appContext.reload()