Skip to content

Commit 48c14ed

Browse files
committed
fix(warn): avoid warning on empty children with Suspense
1 parent de954f4 commit 48c14ed

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', () => {
@@ -645,7 +648,7 @@ describe('Suspense', () => {
645648
<div v-if="errorMessage">{{ errorMessage }}</div>
646649
<Suspense v-else>
647650
<div>
648-
<Async />
651+
<Async />
649652
</div>
650653
<template #fallback>
651654
<div>fallback</div>
@@ -1168,4 +1171,75 @@ describe('Suspense', () => {
11681171
await nextTick()
11691172
expect(serializeInner(root)).toBe(`<div>parent<!----></div>`)
11701173
})
1174+
1175+
describe('warnings', () => {
1176+
// base function to check if a combination of solts warns or not
1177+
function baseCheckWarn(
1178+
sohuldWarn: boolean,
1179+
children: RawSlots,
1180+
props: SuspenseProps | null = null
1181+
) {
1182+
const Comp = {
1183+
setup() {
1184+
return () => h(Suspense, props, children)
1185+
}
1186+
}
1187+
1188+
const root = nodeOps.createElement('div')
1189+
render(h(Comp), root)
1190+
1191+
if (sohuldWarn) {
1192+
expect(`<Suspense> slots expect a single root node.`).toHaveBeenWarned()
1193+
} else {
1194+
expect(
1195+
`<Suspense> slots expect a single root node.`
1196+
).not.toHaveBeenWarned()
1197+
}
1198+
}
1199+
1200+
// actual function that we use in tests
1201+
const checkWarn = baseCheckWarn.bind(null, true)
1202+
const checkNoWarn = baseCheckWarn.bind(null, false)
1203+
1204+
test('does not warn on single child', async () => {
1205+
checkNoWarn({
1206+
default: h('div'),
1207+
fallback: h('div')
1208+
})
1209+
})
1210+
1211+
test('does not warn on null', async () => {
1212+
checkNoWarn({
1213+
default: null,
1214+
fallback: null
1215+
})
1216+
})
1217+
1218+
test('does not warn on <component :is="null" />', async () => {
1219+
checkNoWarn({
1220+
default: () => [resolveDynamicComponent(null)]
1221+
// fallback: () => null
1222+
})
1223+
})
1224+
1225+
test('does not warn on empty array', async () => {
1226+
checkNoWarn({
1227+
default: [],
1228+
fallback: () => []
1229+
})
1230+
})
1231+
1232+
test('warns on multiple children in default', async () => {
1233+
checkWarn({
1234+
default: [h('div'), h('div')]
1235+
})
1236+
})
1237+
1238+
test('warns on multiple children in fallback', async () => {
1239+
checkWarn({
1240+
default: h('div'),
1241+
fallback: [h('div'), h('div')]
1242+
})
1243+
})
1244+
})
11711245
})

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
@@ -744,7 +745,11 @@ function normalizeSuspenseSlot(s: any) {
744745
}
745746
if (isArray(s)) {
746747
const singleChild = filterSingleRoot(s)
747-
if (__DEV__ && !singleChild) {
748+
if (
749+
__DEV__ &&
750+
!singleChild &&
751+
s.filter(child => child !== NULL_DYNAMIC_COMPONENT).length > 0
752+
) {
748753
warn(`<Suspense> slots expect a single root node.`)
749754
}
750755
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)