Skip to content

Commit b46a4dc

Browse files
committed
fix(ssr): handle hydrated async component unmounted before resolve
fix #3787
1 parent b57e995 commit b46a4dc

File tree

3 files changed

+77
-3
lines changed

3 files changed

+77
-3
lines changed

packages/runtime-core/__tests__/hydration.spec.ts

+52-1
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,7 @@ describe('SSR hydration', () => {
626626
expect(spy).toHaveBeenCalled()
627627
})
628628

629-
test('execute the updateComponent(AsyncComponentWrapper) before the async component is resolved', async () => {
629+
test('update async wrapper before resolve', async () => {
630630
const Comp = {
631631
render() {
632632
return h('h1', 'Async component')
@@ -687,6 +687,57 @@ describe('SSR hydration', () => {
687687
)
688688
})
689689

690+
// #3787
691+
test('unmount async wrapper before load', async () => {
692+
let resolve: any
693+
const AsyncComp = defineAsyncComponent(
694+
() =>
695+
new Promise(r => {
696+
resolve = r
697+
})
698+
)
699+
700+
const show = ref(true)
701+
const root = document.createElement('div')
702+
root.innerHTML = '<div><div>async</div></div>'
703+
704+
createSSRApp({
705+
render() {
706+
return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
707+
}
708+
}).mount(root)
709+
710+
show.value = false
711+
await nextTick()
712+
expect(root.innerHTML).toBe('<div><div>hi</div></div>')
713+
resolve({})
714+
})
715+
716+
test('unmount async wrapper before load (fragment)', async () => {
717+
let resolve: any
718+
const AsyncComp = defineAsyncComponent(
719+
() =>
720+
new Promise(r => {
721+
resolve = r
722+
})
723+
)
724+
725+
const show = ref(true)
726+
const root = document.createElement('div')
727+
root.innerHTML = '<div><!--[-->async<!--]--></div>'
728+
729+
createSSRApp({
730+
render() {
731+
return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
732+
}
733+
}).mount(root)
734+
735+
show.value = false
736+
await nextTick()
737+
expect(root.innerHTML).toBe('<div><div>hi</div></div>')
738+
resolve({})
739+
})
740+
690741
test('elements with camel-case in svg ', () => {
691742
const { vnode, container } = mountWithHydration(
692743
'<animateTransform></animateTransform>',

packages/runtime-core/src/hydration.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
Comment,
66
Static,
77
Fragment,
8-
VNodeHook
8+
VNodeHook,
9+
createVNode,
10+
createTextVNode
911
} from './vnode'
1012
import { flushPostFlushCbs } from './scheduler'
1113
import { ComponentInternalInstance } from './component'
@@ -19,6 +21,7 @@ import {
1921
queueEffectWithSuspense
2022
} from './components/Suspense'
2123
import { TeleportImpl, TeleportVNode } from './components/Teleport'
24+
import { isAsyncWrapper } from './apiAsyncComponent'
2225

2326
export type RootHydrateFunction = (
2427
vnode: VNode<Node, Element>,
@@ -187,12 +190,32 @@ export function createHydrationFunctions(
187190
isSVGContainer(container),
188191
optimized
189192
)
193+
190194
// component may be async, so in the case of fragments we cannot rely
191195
// on component's rendered output to determine the end of the fragment
192196
// instead, we do a lookahead to find the end anchor node.
193197
nextNode = isFragmentStart
194198
? locateClosingAsyncAnchor(node)
195199
: nextSibling(node)
200+
201+
// #3787
202+
// if component is async, it may get moved / unmounted before its
203+
// inner component is loaded, so we need to give it a placeholder
204+
// vnode that matches its adopted DOM.
205+
if (isAsyncWrapper(vnode)) {
206+
let subTree
207+
if (isFragmentStart) {
208+
subTree = createVNode(Fragment)
209+
subTree.anchor = nextNode
210+
? nextNode.previousSibling
211+
: container.lastChild
212+
} else {
213+
subTree =
214+
node.nodeType === 3 ? createTextVNode('') : createVNode('div')
215+
}
216+
subTree.el = node
217+
vnode.component!.subTree = subTree
218+
}
196219
} else if (shapeFlag & ShapeFlags.TELEPORT) {
197220
if (domType !== DOMNodeTypes.COMMENT) {
198221
nextNode = onMismatch()

packages/runtime-core/src/renderer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1462,7 +1462,7 @@ function baseCreateRenderer(
14621462
// which means it won't track dependencies - but it's ok because
14631463
// a server-rendered async wrapper is already in resolved state
14641464
// and it will never need to change.
1465-
hydrateSubTree
1465+
() => !instance.isUnmounted && hydrateSubTree()
14661466
)
14671467
} else {
14681468
hydrateSubTree()

0 commit comments

Comments
 (0)