From 45dc2aab73269fdba649a33ef81b3c605b8d16af Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 12 Nov 2024 10:43:05 +0800 Subject: [PATCH 1/3] fix(ssr): avoid updating subtree of async component --- packages/runtime-core/src/hydration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index c49db529c38..87f93b9735c 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -307,7 +307,7 @@ export function createHydrationFunctions( // if component is async, it may get moved / unmounted before its // inner component is loaded, so we need to give it a placeholder // vnode that matches its adopted DOM. - if (isAsyncWrapper(vnode)) { + if (isAsyncWrapper(vnode) && !vnode.component!.subTree) { let subTree if (isFragmentStart) { subTree = createVNode(Fragment) From eb836ba57145bb4bc53142b90355d63c406beeb1 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 12 Nov 2024 12:02:45 +0800 Subject: [PATCH 2/3] test: add test case --- .../runtime-core/__tests__/hydration.spec.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index a1fb8cde33f..797fd2c8e57 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -1284,6 +1284,84 @@ describe('SSR hydration', () => { resolve({}) }) + //#12362 + test('nested async wrapper', async () => { + const Toggle = defineAsyncComponent( + () => + new Promise(r => { + r( + defineComponent({ + setup(_, { slots }) { + const show = ref(false) + onMounted(() => { + nextTick(() => { + show.value = true + }) + }) + return () => + withDirectives( + h('div', null, [renderSlot(slots, 'default')]), + [[vShow, show.value]], + ) + }, + }) as any, + ) + }), + ) + + const Wrapper = defineAsyncComponent(() => { + return new Promise(r => { + r( + defineComponent({ + render(this: any) { + return renderSlot(this.$slots, 'default') + }, + }) as any, + ) + }) + }) + + const count = ref(0) + const fn = vi.fn() + const Child = { + setup() { + onMounted(() => { + fn() + count.value++ + }) + return () => h('div', count.value) + }, + } + + const App = { + render() { + return h(Toggle, null, { + default: () => + h(Wrapper, null, { + default: () => + h(Wrapper, null, { + default: () => h(Child), + }), + }), + }) + }, + } + + const root = document.createElement('div') + root.innerHTML = await renderToString(h(App)) + expect(root.innerHTML).toMatchInlineSnapshot( + `"
0
"`, + ) + + createSSRApp(App).mount(root) + await nextTick() + await nextTick() + expect(root.innerHTML).toMatchInlineSnapshot( + `"
1
"`, + ) + expect(fn).toBeCalledTimes(1) + }) + test('unmount async wrapper before load (fragment)', async () => { let resolve: any const AsyncComp = defineAsyncComponent( From 5c83f47b0a8b5c7108bda0cd8f1152af16655237 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 12 Nov 2024 13:46:24 +0800 Subject: [PATCH 3/3] chore: update --- packages/runtime-core/src/hydration.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 87f93b9735c..69930ff2f93 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -11,7 +11,7 @@ import { normalizeVNode, } from './vnode' import { flushPostFlushCbs } from './scheduler' -import type { ComponentInternalInstance } from './component' +import type { ComponentInternalInstance, ComponentOptions } from './component' import { invokeDirectiveHook } from './directives' import { warn } from './warning' import { @@ -307,7 +307,10 @@ export function createHydrationFunctions( // if component is async, it may get moved / unmounted before its // inner component is loaded, so we need to give it a placeholder // vnode that matches its adopted DOM. - if (isAsyncWrapper(vnode) && !vnode.component!.subTree) { + if ( + isAsyncWrapper(vnode) && + !(vnode.type as ComponentOptions).__asyncResolved + ) { let subTree if (isFragmentStart) { subTree = createVNode(Fragment)