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 = '
async
' + + createSSRApp({ + render() { + return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')]) + } + }).mount(root) + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
hi
') + 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('
hi
') + 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()