From 9dd019854b0ef8a44f0225a4918a5754ffb05045 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 12 Sep 2024 17:47:54 +0800 Subject: [PATCH 1/2] fix(hmr): reload async child wrapped in Suspense + KeepAlive --- packages/runtime-core/__tests__/hmr.spec.ts | 43 +++++++++++++++++++ .../runtime-core/src/components/Suspense.ts | 3 +- packages/runtime-core/src/hmr.ts | 2 + packages/runtime-core/src/renderer.ts | 2 +- 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts index ba9c7c0780b..3f157d009a9 100644 --- a/packages/runtime-core/__tests__/hmr.spec.ts +++ b/packages/runtime-core/__tests__/hmr.spec.ts @@ -851,4 +851,47 @@ describe('hot module replacement', () => { expect(serializeInner(root)).toBe(`
1
1
`) }) + + test('reload async child wrapped in Suspense + KeepAlive', async () => { + const id = 'async-child-reload' + const AsyncChild: ComponentOptions = { + __hmrId: id, + async setup() { + await nextTick() + return () => 'foo' + }, + } + createRecord(id, AsyncChild) + + const appId = 'test-app-id' + const App: ComponentOptions = { + __hmrId: appId, + components: { AsyncChild }, + render: compileToFunction(` +
+ + + + + +
+ `), + } + + const root = nodeOps.createElement('div') + render(h(App), root) + expect(serializeInner(root)).toBe('
') + await timeout() + expect(serializeInner(root)).toBe('
foo
') + + reload(id, { + __hmrId: id, + async setup() { + await nextTick() + return () => 'bar' + }, + }) + await timeout() + expect(serializeInner(root)).toBe('
bar
') + }) }) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 85001f500cf..81eaa915a78 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -31,6 +31,7 @@ import { } from '../warning' import { ErrorCodes, handleError } from '../errorHandling' import { NULL_DYNAMIC_COMPONENT } from '../helpers/resolveAssets' +import { isHmrUpdating } from '../hmr' export interface SuspenseProps { onResolve?: () => void @@ -685,7 +686,7 @@ function createSuspenseBoundary( if (isInPendingSuspense) { suspense.deps++ } - const hydratedEl = instance.vnode.el + const hydratedEl = __DEV__ && isHmrUpdating ? null : instance.vnode.el instance .asyncDep!.catch(err => { handleError(err, instance, ErrorCodes.SETUP_FUNCTION) diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index 0adf4fd4329..7aedf52dd3e 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -144,7 +144,9 @@ 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) }) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 9b50dca23f9..d972c1abde6 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1216,7 +1216,7 @@ function baseCreateRenderer( // Give it a placeholder if this is not hydration // TODO handle self-defined fallback - if (!initialVNode.el) { + if (!initialVNode.el || (__DEV__ && isHmrUpdating)) { const placeholder = (instance.subTree = createVNode(Comment)) processCommentNode(null, placeholder, container!, anchor) } From 19c1762e1b0a7141725dbfec27e4d5607a6a48bb Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 12 Sep 2024 21:30:20 +0800 Subject: [PATCH 2/2] chore: small tweaks --- packages/runtime-core/src/components/Suspense.ts | 3 +-- packages/runtime-core/src/renderer.ts | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 81eaa915a78..85001f500cf 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -31,7 +31,6 @@ import { } from '../warning' import { ErrorCodes, handleError } from '../errorHandling' import { NULL_DYNAMIC_COMPONENT } from '../helpers/resolveAssets' -import { isHmrUpdating } from '../hmr' export interface SuspenseProps { onResolve?: () => void @@ -686,7 +685,7 @@ function createSuspenseBoundary( if (isInPendingSuspense) { suspense.deps++ } - const hydratedEl = __DEV__ && isHmrUpdating ? null : instance.vnode.el + const hydratedEl = instance.vnode.el instance .asyncDep!.catch(err => { handleError(err, instance, ErrorCodes.SETUP_FUNCTION) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index d972c1abde6..90cc22f5470 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1211,12 +1211,15 @@ function baseCreateRenderer( // setup() is async. This component relies on async logic to be resolved // before proceeding if (__FEATURE_SUSPENSE__ && instance.asyncDep) { + // avoid hydration for hmr updating + if (__DEV__ && isHmrUpdating) initialVNode.el = null + parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect, optimized) // Give it a placeholder if this is not hydration // TODO handle self-defined fallback - if (!initialVNode.el || (__DEV__ && isHmrUpdating)) { + if (!initialVNode.el) { const placeholder = (instance.subTree = createVNode(Comment)) processCommentNode(null, placeholder, container!, anchor) }