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(Suspense): skip patch nested suspense if parent suspense is not resolved #10055

Merged
merged 7 commits into from
Jan 11, 2024
Merged
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
135 changes: 135 additions & 0 deletions packages/runtime-core/__tests__/components/Suspense.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1641,6 +1641,141 @@ describe('Suspense', () => {
expect(serializeInner(root)).toBe(expected)
})

//#8678
test('nested suspense (child suspense update before parent suspense resolve)', async () => {
const calls: string[] = []

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

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

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

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

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

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

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

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

expect(serializeInner(root)).toBe(`<div>outerA</div><!---->`)
expect(calls).toEqual([`outerA created`, `outerA mounted`])

// mount inner component
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<div>outerA</div><div>innerA</div>`)

expect(calls).toEqual([
'outerA created',
'outerA mounted',
'innerA created',
'innerA mounted',
])

calls.length = 0
deps.length = 0

// toggle both outer and inner components
outerToggle.value = true
innerToggle.value = true
await nextTick()

await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<div>outerB</div><!---->`)

await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<div>outerB</div><div>innerB</div>`)

// innerB only mount once
expect(calls).toEqual([
'outerB created',
'outerB mounted',
'innerB created',
'innerB mounted',
])
})

// #6416
test('KeepAlive with Suspense', async () => {
const Async = defineAsyncComponent({
Expand Down
12 changes: 12 additions & 0 deletions packages/runtime-core/src/components/Suspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ export const SuspenseImpl = {
rendererInternals,
)
} else {
// #8678 if the current suspense needs to be patched and parentSuspense has
// not been resolved. this means that both the current suspense and parentSuspense
// need to be patched. because parentSuspense's pendingBranch includes the
// current suspense, it will be processed twice:
// 1. current patch
// 2. mounting along with the pendingBranch of parentSuspense
// it is necessary to skip the current patch to avoid multiple mounts
// of inner components.
if (parentSuspense && parentSuspense.deps > 0) {
n2.suspense = n1.suspense
return
}
patchSuspense(
n1,
n2,
Expand Down