Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(runtime-core): Try to fix suspense crash #7275

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions packages/runtime-core/__tests__/hydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,90 @@ describe('SSR hydration', () => {
)
})

test('hydrate safely when property used by 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 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(AsyncWrapper, { 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
30 changes: 18 additions & 12 deletions packages/runtime-core/src/components/Suspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,18 +212,24 @@ function patchSuspense(
if (suspense.deps <= 0) {
suspense.resolve()
} else if (isInFallback) {
patch(
activeBranch,
newFallback,
container,
anchor,
parentComponent,
null, // fallback tree will not have suspense context
isSVG,
slotScopeIds,
optimized
)
setActiveBranch(suspense, newFallback)
// It's possible that the app is in hydrating state when patching the suspense instance
// if someone update the dependency during component setup in children of suspense boundary
// And that would be problemtic because we aren't actually showing a fallback content when patchSuspense is called.
// In such case, patch of fallback content should be no op
if (!isHydrating) {
patch(
activeBranch,
newFallback,
container,
anchor,
parentComponent,
null, // fallback tree will not have suspense context
isSVG,
slotScopeIds,
optimized
)
setActiveBranch(suspense, newFallback)
}
}
} else {
// toggled before pending tree is resolved
Expand Down
23 changes: 23 additions & 0 deletions packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,29 @@ 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
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
}
// and continue the rest of operations once the deps are resolved
instance.subTree.component?.asyncDep?.then(() => {
componentUpdateFn()
})
return
}

// updateComponent
// This is triggered by mutation of component's own state (next: null)
// OR parent calling processComponent (next: VNode)
Expand Down