Skip to content

Commit 6285b81

Browse files
committed
fix(warn): avoid warning on empty children with Suspense
1 parent ab6e927 commit 6285b81

File tree

4 files changed

+84
-4
lines changed

4 files changed

+84
-4
lines changed

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

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ import {
1212
watchEffect,
1313
onUnmounted,
1414
onErrorCaptured,
15-
shallowRef
15+
shallowRef,
16+
SuspenseProps,
17+
resolveDynamicComponent
1618
} from '@vue/runtime-test'
19+
import { RawSlots } from 'packages/runtime-core/src/componentSlots'
1720
import { createApp } from 'vue'
1821

1922
describe('Suspense', () => {
@@ -709,7 +712,7 @@ describe('Suspense', () => {
709712
<div v-if="errorMessage">{{ errorMessage }}</div>
710713
<Suspense v-else>
711714
<div>
712-
<Async />
715+
<Async />
713716
</div>
714717
<template #fallback>
715718
<div>fallback</div>
@@ -1232,4 +1235,75 @@ describe('Suspense', () => {
12321235
await nextTick()
12331236
expect(serializeInner(root)).toBe(`<div>parent<!----></div>`)
12341237
})
1238+
1239+
describe('warnings', () => {
1240+
// base function to check if a combination of solts warns or not
1241+
function baseCheckWarn(
1242+
sohuldWarn: boolean,
1243+
children: RawSlots,
1244+
props: SuspenseProps | null = null
1245+
) {
1246+
const Comp = {
1247+
setup() {
1248+
return () => h(Suspense, props, children)
1249+
}
1250+
}
1251+
1252+
const root = nodeOps.createElement('div')
1253+
render(h(Comp), root)
1254+
1255+
if (sohuldWarn) {
1256+
expect(`<Suspense> slots expect a single root node.`).toHaveBeenWarned()
1257+
} else {
1258+
expect(
1259+
`<Suspense> slots expect a single root node.`
1260+
).not.toHaveBeenWarned()
1261+
}
1262+
}
1263+
1264+
// actual function that we use in tests
1265+
const checkWarn = baseCheckWarn.bind(null, true)
1266+
const checkNoWarn = baseCheckWarn.bind(null, false)
1267+
1268+
test('does not warn on single child', async () => {
1269+
checkNoWarn({
1270+
default: h('div'),
1271+
fallback: h('div')
1272+
})
1273+
})
1274+
1275+
test('does not warn on null', async () => {
1276+
checkNoWarn({
1277+
default: null,
1278+
fallback: null
1279+
})
1280+
})
1281+
1282+
test('does not warn on <component :is="null" />', async () => {
1283+
checkNoWarn({
1284+
default: () => [resolveDynamicComponent(null)]
1285+
// fallback: () => null
1286+
})
1287+
})
1288+
1289+
test('does not warn on empty array', async () => {
1290+
checkNoWarn({
1291+
default: [],
1292+
fallback: () => []
1293+
})
1294+
})
1295+
1296+
test('warns on multiple children in default', async () => {
1297+
checkWarn({
1298+
default: [h('div'), h('div')]
1299+
})
1300+
})
1301+
1302+
test('warns on multiple children in fallback', async () => {
1303+
checkWarn({
1304+
default: h('div'),
1305+
fallback: [h('div'), h('div')]
1306+
})
1307+
})
1308+
})
12351309
})

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { queuePostFlushCb } from '../scheduler'
2222
import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils'
2323
import { pushWarningContext, popWarningContext, warn } from '../warning'
2424
import { handleError, ErrorCodes } from '../errorHandling'
25+
import { NULL_DYNAMIC_COMPONENT } from '../helpers/resolveAssets'
2526

2627
export interface SuspenseProps {
2728
onResolve?: () => void
@@ -750,7 +751,11 @@ function normalizeSuspenseSlot(s: any) {
750751
}
751752
if (isArray(s)) {
752753
const singleChild = filterSingleRoot(s)
753-
if (__DEV__ && !singleChild) {
754+
if (
755+
__DEV__ &&
756+
!singleChild &&
757+
s.filter(child => child !== NULL_DYNAMIC_COMPONENT).length > 0
758+
) {
754759
warn(`<Suspense> slots expect a single root node.`)
755760
}
756761
s = singleChild

packages/runtime-core/src/helpers/resolveAssets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function resolveComponent(
2626
return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name
2727
}
2828

29-
export const NULL_DYNAMIC_COMPONENT = Symbol()
29+
export const NULL_DYNAMIC_COMPONENT = Symbol(__DEV__ ? 'Null' : undefined)
3030

3131
/**
3232
* @private

packages/runtime-core/src/vnode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export type VNodeProps = {
104104

105105
type VNodeChildAtom =
106106
| VNode
107+
| typeof NULL_DYNAMIC_COMPONENT
107108
| string
108109
| number
109110
| boolean

0 commit comments

Comments
 (0)