Skip to content

Commit

Permalink
fix(runtime-core): Fix hydration crash when deep nested async compone…
Browse files Browse the repository at this point in the history
…nt is not hydrated yet
  • Loading branch information
mmis1000 committed Dec 14, 2022
1 parent 77da8ed commit 59cfe63
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 24 deletions.
92 changes: 92 additions & 0 deletions packages/runtime-core/__tests__/hydration.spec.ts
Expand Up @@ -913,6 +913,98 @@ describe('SSR hydration', () => {
`"<!--[--><main><h1 prop="world">Async component</h1><span></span></main><!--]-->"`
)
})

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<void>(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(
`"<!--[--><main><h1 prop="hello">Async component</h1><span></span></main><!--]-->"`
)

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<void>(resolve => {
setTimeout(resolve, 0)
})

expect(bol.value).toBe(false)

// should be hydrated now
// expect(`Hydration node mismatch`).toHaveBeenWarned()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<!--[--><main><h1 prop="world">Async component</h1><span></span></main><!--]-->"`
)
})
// #3787
test('unmount async wrapper before load', async () => {
let resolve: any
Expand Down
64 changes: 40 additions & 24 deletions packages/runtime-core/src/renderer.ts
Expand Up @@ -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
Expand Down

0 comments on commit 59cfe63

Please sign in to comment.