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): Suspense can correctly call the resolved of parent suspense when there is no async deps #8242

Closed
wants to merge 4 commits 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
131 changes: 127 additions & 4 deletions packages/runtime-core/__tests__/components/Suspense.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
shallowRef,
Fragment
} from '@vue/runtime-test'
import { createApp } from 'vue'
import { createApp, defineComponent } from 'vue'

describe('Suspense', () => {
const deps: Promise<any>[] = []
Expand Down Expand Up @@ -1335,9 +1335,14 @@ describe('Suspense', () => {
h(Suspense, null, {
default: [
h(outerToggle.value ? OuterB : OuterA, null, {
default: () => h(Suspense, { suspensible: true },{
default: h(innerToggle.value ? InnerB : InnerA)
})
default: () =>
h(
Suspense,
{ suspensible: true },
{
default: h(innerToggle.value ? InnerB : InnerA)
}
)
})
],
fallback: h('div', 'fallback outer')
Expand Down Expand Up @@ -1400,4 +1405,122 @@ describe('Suspense', () => {
expect(serializeInner(root)).toBe(expected)
expect(calls).toContain('innerB mounted')
})

// #8206
test('nested suspense with suspensible & no async deps', async () => {
const calls: string[] = []
let expected = ''

const InnerA = defineComponent({
setup: () => {
calls.push('innerA created')
onMounted(() => {
calls.push('innerA mounted')
})
return () => h('div', 'innerA')
}
})

const InnerB = defineComponent({
setup: () => {
calls.push('innerB created')
onMounted(() => {
calls.push('innerB mounted')
})
return () => h('div', 'innerB')
}
})

const OuterA = defineComponent({
setup: (_, { slots }: any) => {
calls.push('outerA created')
onMounted(() => {
calls.push('outerA mounted')
})
return () => h(Fragment, null, [h('div', 'outerA'), slots.default?.()])
}
})

const OuterB = defineComponent({
setup: (_, { slots }: any) => {
calls.push('outerB created')
onMounted(() => {
calls.push('outerB mounted')
})
return () => h(Fragment, null, [h('div', 'outerB'), slots.default?.()])
}
})

const outerToggle = ref(false)
const innerToggle = ref(false)

/**
* <Suspense>
* <component :is="outerToggle ? outerB : outerA">
* <Suspense suspensible>
* <component :is="innerToggle ? innerB : innerA" />
* </Suspense>
* </component>
* </Suspense>
*/
const Comp = defineComponent({
setup() {
return () =>
h(Suspense, null, {
default: [
h(outerToggle.value ? OuterB : OuterA, null, {
default: () =>
h(
Suspense,
{ suspensible: true },
{
default: h(innerToggle.value ? InnerB : InnerA)
}
)
})
],
fallback: h('div', 'fallback outer')
})
}
})

expected = `<div>outerA</div><div>innerA</div>`
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(expected)

// mount outer component and inner component
await Promise.all(deps)
await nextTick()

expect(serializeInner(root)).toBe(expected)
expect(calls).toEqual([
'outerA created',
'innerA created',
'innerA mounted',
'outerA mounted'
])

// toggle outer component
calls.length = 0
deps.length = 0
outerToggle.value = true
await nextTick()

await Promise.all(deps)
await nextTick()
expected = `<div>outerB</div><div>innerA</div>`
expect(serializeInner(root)).toBe(expected)
expect(calls).toContain('outerB mounted')
expect(calls).toContain('innerA mounted')

// toggle inner component
calls.length = 0
deps.length = 0
innerToggle.value = true
await Promise.all(deps)
await nextTick()
expected = `<div>outerB</div><div>innerB</div>`
expect(serializeInner(root)).toBe(expected)
})
})
15 changes: 13 additions & 2 deletions packages/runtime-core/src/components/Suspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ function mountSuspense(
isSVG,
slotScopeIds
)
// suspense.deps <= 0 and suspensible is true
// Explain that this suspense has no async deps,
// parentSuspense.deps subtracts the number of deps added before,
// to avoid wrongly calling parentSuspense.resolved
if (suspense.deps <= 0 && isVNodeSuspensible(vnode) && parentSuspense) {
parentSuspense.deps--
}

// now check if we have encountered any async deps
if (suspense.deps > 0) {
// has async
Expand Down Expand Up @@ -437,8 +445,7 @@ function createSuspenseBoundary(

// if set `suspensible: true`, set the current suspense as a dep of parent suspense
let parentSuspenseId: number | undefined
const isSuspensible =
vnode.props?.suspensible != null && vnode.props.suspensible !== false
const isSuspensible = isVNodeSuspensible(vnode)
if (isSuspensible) {
if (parentSuspense?.pendingBranch) {
parentSuspenseId = parentSuspense?.pendingId
Expand Down Expand Up @@ -831,3 +838,7 @@ function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) {
updateHOCHostEl(parentComponent, el)
}
}

function isVNodeSuspensible(vnode: VNode) {
return vnode.props?.suspensible != null && vnode.props.suspensible !== false
}