Skip to content

Commit c8d9683

Browse files
authored
fix(hydration): fix update before async component is hydrated (#3563)
fix #3560
1 parent 43f7815 commit c8d9683

File tree

3 files changed

+111
-39
lines changed

3 files changed

+111
-39
lines changed

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

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

629+
test('execute the updateComponent(AsyncComponentWrapper) before the async component is resolved', async () => {
630+
const Comp = {
631+
render() {
632+
return h('h1', 'Async component')
633+
}
634+
}
635+
let serverResolve: any
636+
let AsyncComp = defineAsyncComponent(
637+
() =>
638+
new Promise(r => {
639+
serverResolve = r
640+
})
641+
)
642+
643+
const bol = ref(true)
644+
const App = {
645+
setup() {
646+
onMounted(() => {
647+
// change state, this makes updateComponent(AsyncComp) execute before
648+
// the async component is resolved
649+
bol.value = false
650+
})
651+
652+
return () => {
653+
return [bol.value ? 'hello' : 'world', h(AsyncComp)]
654+
}
655+
}
656+
}
657+
658+
// server render
659+
const htmlPromise = renderToString(h(App))
660+
serverResolve(Comp)
661+
const html = await htmlPromise
662+
expect(html).toMatchInlineSnapshot(
663+
`"<!--[-->hello<h1>Async component</h1><!--]-->"`
664+
)
665+
666+
// hydration
667+
let clientResolve: any
668+
AsyncComp = defineAsyncComponent(
669+
() =>
670+
new Promise(r => {
671+
clientResolve = r
672+
})
673+
)
674+
675+
const container = document.createElement('div')
676+
container.innerHTML = html
677+
createSSRApp(App).mount(container)
678+
679+
// resolve
680+
clientResolve(Comp)
681+
await new Promise(r => setTimeout(r))
682+
683+
// should be hydrated now
684+
expect(`Hydration node mismatch`).not.toHaveBeenWarned()
685+
expect(container.innerHTML).toMatchInlineSnapshot(
686+
`"<!--[-->world<h1>Async component</h1><!--]-->"`
687+
)
688+
})
689+
629690
test('elements with camel-case in svg ', () => {
630691
const { vnode, container } = mountWithHydration(
631692
'<animateTransform></animateTransform>',

packages/runtime-core/src/hydration.ts

+10-19
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
VNodeHook
99
} from './vnode'
1010
import { flushPostFlushCbs } from './scheduler'
11-
import { ComponentOptions, ComponentInternalInstance } from './component'
11+
import { ComponentInternalInstance } from './component'
1212
import { invokeDirectiveHook } from './directives'
1313
import { warn } from './warning'
1414
import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared'
@@ -178,24 +178,15 @@ export function createHydrationFunctions(
178178
// on its sub-tree.
179179
vnode.slotScopeIds = slotScopeIds
180180
const container = parentNode(node)!
181-
const hydrateComponent = () => {
182-
mountComponent(
183-
vnode,
184-
container,
185-
null,
186-
parentComponent,
187-
parentSuspense,
188-
isSVGContainer(container),
189-
optimized
190-
)
191-
}
192-
// async component
193-
const loadAsync = (vnode.type as ComponentOptions).__asyncLoader
194-
if (loadAsync) {
195-
loadAsync().then(hydrateComponent)
196-
} else {
197-
hydrateComponent()
198-
}
181+
mountComponent(
182+
vnode,
183+
container,
184+
null,
185+
parentComponent,
186+
parentSuspense,
187+
isSVGContainer(container),
188+
optimized
189+
)
199190
// component may be async, so in the case of fragments we cannot rely
200191
// on component's rendered output to determine the end of the fragment
201192
// instead, we do a lookahead to find the end anchor node.

packages/runtime-core/src/renderer.ts

+40-20
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from './vnode'
1717
import {
1818
ComponentInternalInstance,
19+
ComponentOptions,
1920
createComponentInstance,
2021
Data,
2122
setupComponent
@@ -1430,31 +1431,50 @@ function baseCreateRenderer(
14301431
instance.emit('hook:beforeMount')
14311432
}
14321433

1433-
// render
1434-
if (__DEV__) {
1435-
startMeasure(instance, `render`)
1436-
}
1437-
const subTree = (instance.subTree = renderComponentRoot(instance))
1438-
if (__DEV__) {
1439-
endMeasure(instance, `render`)
1440-
}
1441-
14421434
if (el && hydrateNode) {
1435+
// vnode has adopted host node - perform hydration instead of mount.
1436+
const hydrateSubTree = () => {
1437+
if (__DEV__) {
1438+
startMeasure(instance, `render`)
1439+
}
1440+
instance.subTree = renderComponentRoot(instance)
1441+
if (__DEV__) {
1442+
endMeasure(instance, `render`)
1443+
}
1444+
if (__DEV__) {
1445+
startMeasure(instance, `hydrate`)
1446+
}
1447+
hydrateNode!(
1448+
el as Node,
1449+
instance.subTree,
1450+
instance,
1451+
parentSuspense,
1452+
null
1453+
)
1454+
if (__DEV__) {
1455+
endMeasure(instance, `hydrate`)
1456+
}
1457+
}
1458+
1459+
if (isAsyncWrapper(initialVNode)) {
1460+
(initialVNode.type as ComponentOptions).__asyncLoader!().then(
1461+
// note: we are moving the render call into an async callback,
1462+
// which means it won't track dependencies - but it's ok because
1463+
// a server-rendered async wrapper is already in resolved state
1464+
// and it will never need to change.
1465+
hydrateSubTree
1466+
)
1467+
} else {
1468+
hydrateSubTree()
1469+
}
1470+
} else {
14431471
if (__DEV__) {
1444-
startMeasure(instance, `hydrate`)
1472+
startMeasure(instance, `render`)
14451473
}
1446-
// vnode has adopted host node - perform hydration instead of mount.
1447-
hydrateNode(
1448-
initialVNode.el as Node,
1449-
subTree,
1450-
instance,
1451-
parentSuspense,
1452-
null
1453-
)
1474+
const subTree = (instance.subTree = renderComponentRoot(instance))
14541475
if (__DEV__) {
1455-
endMeasure(instance, `hydrate`)
1476+
endMeasure(instance, `render`)
14561477
}
1457-
} else {
14581478
if (__DEV__) {
14591479
startMeasure(instance, `patch`)
14601480
}

0 commit comments

Comments
 (0)