diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts
index ae152ba694f..a1e1886ec39 100644
--- a/packages/runtime-core/__tests__/hydration.spec.ts
+++ b/packages/runtime-core/__tests__/hydration.spec.ts
@@ -913,6 +913,98 @@ describe('SSR hydration', () => {
`"Async component
"`
)
})
+
+ test('hydrate safely when property used by deep nested async setup changed before render', async () => {
+ // let serverResolve: any
+
+ const AsyncComp = {
+ async setup() {
+ await new Promise(r => r())
+ return () => {
+ return h('h1', 'Async component')
+ }
+ }
+ }
+
+ const AsyncWrapper = {
+ render() {
+ return h(AsyncComp)
+ }
+ }
+ const AsyncWrapperWrapper = {
+ render() {
+ return h(AsyncWrapper)
+ }
+ }
+
+ const SiblingComp = {
+ setup() {
+ return () => {
+ bol.value = false
+ return h('span')
+ }
+ }
+ }
+
+ const bol = ref(true)
+
+ const App = {
+ setup() {
+ return () => {
+ return [
+ h(
+ Suspense,
+ {},
+ {
+ default: () => {
+ return [
+ h('main', {}, [
+ h(AsyncWrapperWrapper, {
+ prop: bol.value ? 'hello' : 'world'
+ }),
+ h(SiblingComp)
+ ])
+ ]
+ }
+ }
+ )
+ ]
+ }
+ }
+ }
+
+ // server render
+
+ const html = await renderToString(h(App))
+
+ expect(html).toMatchInlineSnapshot(
+ `"Async component
"`
+ )
+
+ expect(bol.value).toBe(false)
+
+ // hydration
+
+ // reset the value
+ bol.value = true
+ expect(bol.value).toBe(true)
+
+ const container = document.createElement('div')
+ container.innerHTML = html
+ createSSRApp(App).mount(container)
+
+ await new Promise(resolve => {
+ setTimeout(resolve, 0)
+ })
+
+ expect(bol.value).toBe(false)
+
+ // should be hydrated now
+ // expect(`Hydration node mismatch`).toHaveBeenWarned()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"Async component
"`
+ )
+ })
// #3787
test('unmount async wrapper before load', async () => {
let resolve: any
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index d46cb5e915e..c26f585209c 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -1436,32 +1436,48 @@ function baseCreateRenderer(
// #2458: deference mount-only object parameters to prevent memleaks
initialVNode = container = anchor = null as any
} else {
- // we are trying to update some async comp before hydration
- // this will cause crash because we don't know the root node yet
- if (
- __FEATURE_SUSPENSE__ &&
- instance.subTree.shapeFlag & ShapeFlags.COMPONENT &&
- // this happens only during hydration
- instance.subTree.component?.subTree == null &&
- // we don't know the subTree yet because we haven't resolve it
- instance.subTree.component?.asyncResolved === false
- ) {
- // only sync the properties and abort the rest of operations
- let { next, vnode } = instance
- toggleRecurse(instance, false)
- if (next) {
- next.el = vnode.el
- updateComponentPreRender(instance, next, optimized)
+ if (__FEATURE_SUSPENSE__) {
+ const locateNonHydratedAsyncRoot = (
+ instance: ComponentInternalInstance
+ ): ComponentInternalInstance | null => {
+ if (instance.subTree.shapeFlag & ShapeFlags.COMPONENT) {
+ if (
+ // this happens only during hydration
+ instance.subTree.component?.subTree == null &&
+ // we don't know the subTree yet because we haven't resolve it
+ instance.subTree.component?.asyncResolved === false
+ ) {
+ return instance.subTree.component!
+ } else {
+ return locateNonHydratedAsyncRoot(instance.subTree.component!)
+ }
+ } else {
+ return null
+ }
}
- toggleRecurse(instance, true)
- // and continue the rest of operations once the deps are resolved
- instance.subTree.component?.asyncDep?.then(() => {
- // the instance may be destroyed during the time period
- if (!instance.isUnmounted) {
- componentUpdateFn()
+
+ const nonHydratedAsyncRoot = locateNonHydratedAsyncRoot(instance)
+
+ // we are trying to update some async comp before hydration
+ // this will cause crash because we don't know the root node yet
+ if (nonHydratedAsyncRoot != null) {
+ // only sync the properties and abort the rest of operations
+ let { next, vnode } = instance
+ toggleRecurse(instance, false)
+ if (next) {
+ next.el = vnode.el
+ updateComponentPreRender(instance, next, optimized)
}
- })
- return
+ toggleRecurse(instance, true)
+ // and continue the rest of operations once the deps are resolved
+ nonHydratedAsyncRoot.asyncDep?.then(() => {
+ // the instance may be destroyed during the time period
+ if (!instance.isUnmounted) {
+ componentUpdateFn()
+ }
+ })
+ return
+ }
}
// updateComponent