Skip to content

Commit

Permalink
fix(ssr): handle hydrated async component unmounted before resolve
Browse files Browse the repository at this point in the history
fix #3787
  • Loading branch information
yyx990803 committed May 26, 2021
1 parent b57e995 commit b46a4dc
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 3 deletions.
53 changes: 52 additions & 1 deletion packages/runtime-core/__tests__/hydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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 = '<div><div>async</div></div>'

createSSRApp({
render() {
return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
}
}).mount(root)

show.value = false
await nextTick()
expect(root.innerHTML).toBe('<div><div>hi</div></div>')
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 = '<div><!--[-->async<!--]--></div>'

createSSRApp({
render() {
return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
}
}).mount(root)

show.value = false
await nextTick()
expect(root.innerHTML).toBe('<div><div>hi</div></div>')
resolve({})
})

test('elements with camel-case in svg ', () => {
const { vnode, container } = mountWithHydration(
'<animateTransform></animateTransform>',
Expand Down
25 changes: 24 additions & 1 deletion packages/runtime-core/src/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
Comment,
Static,
Fragment,
VNodeHook
VNodeHook,
createVNode,
createTextVNode
} from './vnode'
import { flushPostFlushCbs } from './scheduler'
import { ComponentInternalInstance } from './component'
Expand All @@ -19,6 +21,7 @@ import {
queueEffectWithSuspense
} from './components/Suspense'
import { TeleportImpl, TeleportVNode } from './components/Teleport'
import { isAsyncWrapper } from './apiAsyncComponent'

export type RootHydrateFunction = (
vnode: VNode<Node, Element>,
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit b46a4dc

Please sign in to comment.