From ef20b86b36a127e317f8981df970dc8efd277053 Mon Sep 17 00:00:00 2001 From: edison Date: Tue, 2 Sep 2025 17:07:36 +0800 Subject: [PATCH] fix(hmr): prevent update unmounting component during HMR reload (#13815) close vitejs/vite-plugin-vue#599 --- packages/runtime-core/__tests__/hmr.spec.ts | 51 +++++++++++++++++++++ packages/runtime-core/src/hmr.ts | 14 ++++-- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts index 01feb91ace..e8e0398e17 100644 --- a/packages/runtime-core/__tests__/hmr.spec.ts +++ b/packages/runtime-core/__tests__/hmr.spec.ts @@ -937,4 +937,55 @@ describe('hot module replacement', () => { rerender(id, () => 'bar') expect(serializeInner(root)).toBe('bar') }) + + // https://github.com/vitejs/vite-plugin-vue/issues/599 + // Both Outer and Inner are reloaded when './server.js' changes + test('reload nested components from single update', async () => { + const innerId = 'nested-reload-inner' + const outerId = 'nested-reload-outer' + + let Inner = { + __hmrId: innerId, + render() { + return h('div', 'foo') + }, + } + let Outer = { + __hmrId: outerId, + render() { + return h(Inner) + }, + } + + createRecord(innerId, Inner) + createRecord(outerId, Outer) + + const App = { + render: () => h(Outer), + } + + const root = nodeOps.createElement('div') + render(h(App), root) + expect(serializeInner(root)).toBe('
foo
') + + Inner = { + __hmrId: innerId, + render() { + return h('div', 'bar') + }, + } + Outer = { + __hmrId: outerId, + render() { + return h(Inner) + }, + } + + // trigger reload for both Outer and Inner + reload(outerId, Outer) + reload(innerId, Inner) + await nextTick() + + expect(serializeInner(root)).toBe('
bar
') + }) }) diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index 2c46ea73b5..4191a34f82 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -147,11 +147,15 @@ function reload(id: string, newComp: HMRComponent): void { // 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. queueJob(() => { - isHmrUpdating = true - instance.parent!.update() - isHmrUpdating = false - // #6930, #11248 avoid infinite recursion - dirtyInstances.delete(instance) + // vite-plugin-vue/issues/599 + // don't update if the job is already disposed + if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) { + isHmrUpdating = true + instance.parent!.update() + isHmrUpdating = false + // #6930, #11248 avoid infinite recursion + dirtyInstances.delete(instance) + } }) } else if (instance.appContext.reload) { // root instance mounted via createApp() has a reload method -- 2.47.3