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()