Skip to content

Commit e147512

Browse files
committed
fix(suspense): fix nested suspensible suspense with no asyn deps
close #8206
1 parent 62e71b5 commit e147512

File tree

2 files changed

+137
-11
lines changed

2 files changed

+137
-11
lines changed

packages/runtime-core/__tests__/components/Suspense.spec.ts

+127-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
shallowRef,
2020
Fragment
2121
} from '@vue/runtime-test'
22-
import { createApp } from 'vue'
22+
import { createApp, defineComponent } from 'vue'
2323

2424
describe('Suspense', () => {
2525
const deps: Promise<any>[] = []
@@ -1335,9 +1335,14 @@ describe('Suspense', () => {
13351335
h(Suspense, null, {
13361336
default: [
13371337
h(outerToggle.value ? OuterB : OuterA, null, {
1338-
default: () => h(Suspense, { suspensible: true },{
1339-
default: h(innerToggle.value ? InnerB : InnerA)
1340-
})
1338+
default: () =>
1339+
h(
1340+
Suspense,
1341+
{ suspensible: true },
1342+
{
1343+
default: h(innerToggle.value ? InnerB : InnerA)
1344+
}
1345+
)
13411346
})
13421347
],
13431348
fallback: h('div', 'fallback outer')
@@ -1400,4 +1405,122 @@ describe('Suspense', () => {
14001405
expect(serializeInner(root)).toBe(expected)
14011406
expect(calls).toContain('innerB mounted')
14021407
})
1408+
1409+
// #8206
1410+
test('nested suspense with suspensible & no async deps', async () => {
1411+
const calls: string[] = []
1412+
let expected = ''
1413+
1414+
const InnerA = defineComponent({
1415+
setup: () => {
1416+
calls.push('innerA created')
1417+
onMounted(() => {
1418+
calls.push('innerA mounted')
1419+
})
1420+
return () => h('div', 'innerA')
1421+
}
1422+
})
1423+
1424+
const InnerB = defineComponent({
1425+
setup: () => {
1426+
calls.push('innerB created')
1427+
onMounted(() => {
1428+
calls.push('innerB mounted')
1429+
})
1430+
return () => h('div', 'innerB')
1431+
}
1432+
})
1433+
1434+
const OuterA = defineComponent({
1435+
setup: (_, { slots }: any) => {
1436+
calls.push('outerA created')
1437+
onMounted(() => {
1438+
calls.push('outerA mounted')
1439+
})
1440+
return () => h(Fragment, null, [h('div', 'outerA'), slots.default?.()])
1441+
}
1442+
})
1443+
1444+
const OuterB = defineComponent({
1445+
setup: (_, { slots }: any) => {
1446+
calls.push('outerB created')
1447+
onMounted(() => {
1448+
calls.push('outerB mounted')
1449+
})
1450+
return () => h(Fragment, null, [h('div', 'outerB'), slots.default?.()])
1451+
}
1452+
})
1453+
1454+
const outerToggle = ref(false)
1455+
const innerToggle = ref(false)
1456+
1457+
/**
1458+
* <Suspense>
1459+
* <component :is="outerToggle ? outerB : outerA">
1460+
* <Suspense suspensible>
1461+
* <component :is="innerToggle ? innerB : innerA" />
1462+
* </Suspense>
1463+
* </component>
1464+
* </Suspense>
1465+
*/
1466+
const Comp = defineComponent({
1467+
setup() {
1468+
return () =>
1469+
h(Suspense, null, {
1470+
default: [
1471+
h(outerToggle.value ? OuterB : OuterA, null, {
1472+
default: () =>
1473+
h(
1474+
Suspense,
1475+
{ suspensible: true },
1476+
{
1477+
default: h(innerToggle.value ? InnerB : InnerA)
1478+
}
1479+
)
1480+
})
1481+
],
1482+
fallback: h('div', 'fallback outer')
1483+
})
1484+
}
1485+
})
1486+
1487+
expected = `<div>outerA</div><div>innerA</div>`
1488+
const root = nodeOps.createElement('div')
1489+
render(h(Comp), root)
1490+
expect(serializeInner(root)).toBe(expected)
1491+
1492+
// mount outer component and inner component
1493+
await Promise.all(deps)
1494+
await nextTick()
1495+
1496+
expect(serializeInner(root)).toBe(expected)
1497+
expect(calls).toEqual([
1498+
'outerA created',
1499+
'innerA created',
1500+
'innerA mounted',
1501+
'outerA mounted'
1502+
])
1503+
1504+
// toggle outer component
1505+
calls.length = 0
1506+
deps.length = 0
1507+
outerToggle.value = true
1508+
await nextTick()
1509+
1510+
await Promise.all(deps)
1511+
await nextTick()
1512+
expected = `<div>outerB</div><div>innerA</div>`
1513+
expect(serializeInner(root)).toBe(expected)
1514+
expect(calls).toContain('outerB mounted')
1515+
expect(calls).toContain('innerA mounted')
1516+
1517+
// toggle inner component
1518+
calls.length = 0
1519+
deps.length = 0
1520+
innerToggle.value = true
1521+
await Promise.all(deps)
1522+
await nextTick()
1523+
expected = `<div>outerB</div><div>innerB</div>`
1524+
expect(serializeInner(root)).toBe(expected)
1525+
})
14031526
})

packages/runtime-core/src/components/Suspense.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ function mountSuspense(
184184
setActiveBranch(suspense, vnode.ssFallback!)
185185
} else {
186186
// Suspense has no async deps. Just resolve.
187-
suspense.resolve()
187+
suspense.resolve(false, true)
188188
}
189189
}
190190

@@ -388,7 +388,7 @@ export interface SuspenseBoundary {
388388
isHydrating: boolean
389389
isUnmounted: boolean
390390
effects: Function[]
391-
resolve(force?: boolean): void
391+
resolve(force?: boolean, sync?: boolean): void
392392
fallback(fallbackVNode: VNode): void
393393
move(
394394
container: RendererElement,
@@ -437,11 +437,10 @@ function createSuspenseBoundary(
437437

438438
// if set `suspensible: true`, set the current suspense as a dep of parent suspense
439439
let parentSuspenseId: number | undefined
440-
const isSuspensible =
441-
vnode.props?.suspensible != null && vnode.props.suspensible !== false
440+
const isSuspensible = isVNodeSuspensible(vnode)
442441
if (isSuspensible) {
443442
if (parentSuspense?.pendingBranch) {
444-
parentSuspenseId = parentSuspense?.pendingId
443+
parentSuspenseId = parentSuspense.pendingId
445444
parentSuspense.deps++
446445
}
447446
}
@@ -469,7 +468,7 @@ function createSuspenseBoundary(
469468
isUnmounted: false,
470469
effects: [],
471470

472-
resolve(resume = false) {
471+
resolve(resume = false, sync = false) {
473472
if (__DEV__) {
474473
if (!resume && !suspense.pendingBranch) {
475474
throw new Error(
@@ -553,7 +552,7 @@ function createSuspenseBoundary(
553552
parentSuspenseId === parentSuspense.pendingId
554553
) {
555554
parentSuspense.deps--
556-
if (parentSuspense.deps === 0) {
555+
if (parentSuspense.deps === 0 && !sync) {
557556
parentSuspense.resolve()
558557
}
559558
}
@@ -831,3 +830,7 @@ function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) {
831830
updateHOCHostEl(parentComponent, el)
832831
}
833832
}
833+
834+
function isVNodeSuspensible(vnode: VNode) {
835+
return vnode.props?.suspensible != null && vnode.props.suspensible !== false
836+
}

0 commit comments

Comments
 (0)