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( diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index c49db529c38..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)) { + if ( + isAsyncWrapper(vnode) && + !(vnode.type as ComponentOptions).__asyncResolved + ) { let subTree if (isFragmentStart) { subTree = createVNode(Fragment)