diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts
index 8db566b5..c55a9ab6 100644
--- a/packages/runtime-core/__tests__/hydration.spec.ts
+++ b/packages/runtime-core/__tests__/hydration.spec.ts
@@ -626,7 +626,7 @@ describe('SSR hydration', () => {
expect(spy).toHaveBeenCalled()
})
- test('execute the updateComponent(AsyncComponentWrapper) before the async component is resolved', async () => {
+ test('update async wrapper before resolve', async () => {
const Comp = {
render() {
return h('h1', 'Async component')
@@ -687,6 +687,57 @@ describe('SSR hydration', () => {
)
})
+ // #3787
+ test('unmount async wrapper before load', async () => {
+ let resolve: any
+ const AsyncComp = defineAsyncComponent(
+ () =>
+ new Promise(r => {
+ resolve = r
+ })
+ )
+
+ const show = ref(true)
+ const root = document.createElement('div')
+ root.innerHTML = '
'
+
+ createSSRApp({
+ render() {
+ return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
+ }
+ }).mount(root)
+
+ show.value = false
+ await nextTick()
+ expect(root.innerHTML).toBe('')
+ resolve({})
+ })
+
+ test('unmount async wrapper before load (fragment)', async () => {
+ let resolve: any
+ const AsyncComp = defineAsyncComponent(
+ () =>
+ new Promise(r => {
+ resolve = r
+ })
+ )
+
+ const show = ref(true)
+ const root = document.createElement('div')
+ root.innerHTML = 'async
'
+
+ createSSRApp({
+ render() {
+ return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
+ }
+ }).mount(root)
+
+ show.value = false
+ await nextTick()
+ expect(root.innerHTML).toBe('')
+ resolve({})
+ })
+
test('elements with camel-case in svg ', () => {
const { vnode, container } = mountWithHydration(
'',
diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts
index 45b6d7b0..f4f7ea47 100644
--- a/packages/runtime-core/src/hydration.ts
+++ b/packages/runtime-core/src/hydration.ts
@@ -5,7 +5,9 @@ import {
Comment,
Static,
Fragment,
- VNodeHook
+ VNodeHook,
+ createVNode,
+ createTextVNode
} from './vnode'
import { flushPostFlushCbs } from './scheduler'
import { ComponentInternalInstance } from './component'
@@ -19,6 +21,7 @@ import {
queueEffectWithSuspense
} from './components/Suspense'
import { TeleportImpl, TeleportVNode } from './components/Teleport'
+import { isAsyncWrapper } from './apiAsyncComponent'
export type RootHydrateFunction = (
vnode: VNode,
@@ -187,12 +190,32 @@ export function createHydrationFunctions(
isSVGContainer(container),
optimized
)
+
// component may be async, so in the case of fragments we cannot rely
// on component's rendered output to determine the end of the fragment
// instead, we do a lookahead to find the end anchor node.
nextNode = isFragmentStart
? locateClosingAsyncAnchor(node)
: nextSibling(node)
+
+ // #3787
+ // 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)) {
+ let subTree
+ if (isFragmentStart) {
+ subTree = createVNode(Fragment)
+ subTree.anchor = nextNode
+ ? nextNode.previousSibling
+ : container.lastChild
+ } else {
+ subTree =
+ node.nodeType === 3 ? createTextVNode('') : createVNode('div')
+ }
+ subTree.el = node
+ vnode.component!.subTree = subTree
+ }
} else if (shapeFlag & ShapeFlags.TELEPORT) {
if (domType !== DOMNodeTypes.COMMENT) {
nextNode = onMismatch()
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index 4e661bc9..1da8cd09 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -1462,7 +1462,7 @@ function baseCreateRenderer(
// which means it won't track dependencies - but it's ok because
// a server-rendered async wrapper is already in resolved state
// and it will never need to change.
- hydrateSubTree
+ () => !instance.isUnmounted && hydrateSubTree()
)
} else {
hydrateSubTree()