Add warning when single item or nested arrays are used with SuspenseList (#16094)

This commit is contained in:
Sebastian Markbåge 2019-07-10 11:07:28 -07:00 committed by GitHub
parent 2073a7144e
commit 48f6594474
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 149 additions and 2 deletions

View File

@ -67,7 +67,7 @@ import shallowEqual from 'shared/shallowEqual';
import getComponentName from 'shared/getComponentName';
import ReactStrictModeWarnings from './ReactStrictModeWarnings';
import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent';
import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
import {REACT_LAZY_TYPE, getIteratorFn} from 'shared/ReactSymbols';
import warning from 'shared/warning';
import warningWithoutStack from 'shared/warningWithoutStack';
import {
@ -2094,6 +2094,72 @@ function validateTailOptions(
}
}
function validateSuspenseListNestedChild(childSlot: mixed, index: number) {
if (__DEV__) {
let isArray = Array.isArray(childSlot);
let isIterable = !isArray && typeof getIteratorFn(childSlot) === 'function';
if (isArray || isIterable) {
let type = isArray ? 'array' : 'iterable';
warning(
false,
'A nested %s was passed to row #%s in <SuspenseList />. Wrap it in ' +
'an additional SuspenseList to configure its revealOrder: ' +
'<SuspenseList revealOrder=...> ... ' +
'<SuspenseList revealOrder=...>{%s}</SuspenseList> ... ' +
'</SuspenseList>',
type,
index,
type,
);
return false;
}
}
return true;
}
function validateSuspenseListChildren(
children: mixed,
revealOrder: SuspenseListRevealOrder,
) {
if (__DEV__) {
if (
(revealOrder === 'forwards' || revealOrder === 'backwards') &&
(children !== undefined && children !== null && children !== false)
) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
if (!validateSuspenseListNestedChild(children[i], i)) {
return;
}
}
} else {
let iteratorFn = getIteratorFn(children);
if (typeof iteratorFn === 'function') {
const childrenIterator = iteratorFn.call(children);
if (childrenIterator) {
let step = childrenIterator.next();
let i = 0;
for (; !step.done; step = childrenIterator.next()) {
if (!validateSuspenseListNestedChild(step.value, i)) {
return;
}
i++;
}
}
} else {
warning(
false,
'A single row was passed to a <SuspenseList revealOrder="%s" />. ' +
'This is not useful since it needs multiple rows. ' +
'Did you mean to pass multiple children or an array?',
revealOrder,
);
}
}
}
}
}
function initSuspenseListRenderState(
workInProgress: Fiber,
isBackwards: boolean,
@ -2142,6 +2208,7 @@ function updateSuspenseListComponent(
validateRevealOrder(revealOrder);
validateTailOptions(tailMode, revealOrder);
validateSuspenseListChildren(newChildren, revealOrder);
reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);

View File

@ -101,6 +101,85 @@ describe('ReactSuspenseList', () => {
]);
});
it('warns if a single element is passed to a "forwards" list', () => {
function Foo({children}) {
return <SuspenseList revealOrder="forwards">{children}</SuspenseList>;
}
ReactNoop.render(<Foo />);
// No warning
Scheduler.unstable_flushAll();
ReactNoop.render(<Foo>{null}</Foo>);
// No warning
Scheduler.unstable_flushAll();
ReactNoop.render(<Foo>{false}</Foo>);
// No warning
Scheduler.unstable_flushAll();
ReactNoop.render(
<Foo>
<Suspense fallback="Loading">Child</Suspense>
</Foo>,
);
expect(() => Scheduler.unstable_flushAll()).toWarnDev([
'Warning: A single row was passed to a <SuspenseList revealOrder="forwards" />. ' +
'This is not useful since it needs multiple rows. ' +
'Did you mean to pass multiple children or an array?' +
'\n in SuspenseList (at **)' +
'\n in Foo (at **)',
]);
});
it('warns if a single fragment is passed to a "backwards" list', () => {
function Foo() {
return (
<SuspenseList revealOrder="backwards">
<Fragment>{[]}</Fragment>
</SuspenseList>
);
}
ReactNoop.render(<Foo />);
expect(() => Scheduler.unstable_flushAll()).toWarnDev([
'Warning: A single row was passed to a <SuspenseList revealOrder="backwards" />. ' +
'This is not useful since it needs multiple rows. ' +
'Did you mean to pass multiple children or an array?' +
'\n in SuspenseList (at **)' +
'\n in Foo (at **)',
]);
});
it('warns if a nested array is passed to a "forwards" list', () => {
function Foo({items}) {
return (
<SuspenseList revealOrder="forwards">
{items.map(name => (
<Suspense key={name} fallback="Loading">
{name}
</Suspense>
))}
<div>Tail</div>
</SuspenseList>
);
}
ReactNoop.render(<Foo items={['A', 'B']} />);
expect(() => Scheduler.unstable_flushAll()).toWarnDev([
'Warning: A nested array was passed to row #0 in <SuspenseList />. ' +
'Wrap it in an additional SuspenseList to configure its revealOrder: ' +
'<SuspenseList revealOrder=...> ... ' +
'<SuspenseList revealOrder=...>{array}</SuspenseList> ... ' +
'</SuspenseList>' +
'\n in SuspenseList (at **)' +
'\n in Foo (at **)',
]);
});
it('shows content independently by default', async () => {
let A = createAsyncText('A');
let B = createAsyncText('B');
@ -1162,7 +1241,8 @@ describe('ReactSuspenseList', () => {
function Foo() {
return (
<SuspenseList revealOrder="forwards" tail="collapse">
<Suspense fallback="Loading">Content</Suspense>
<Suspense fallback="Loading">A</Suspense>
<Suspense fallback="Loading">B</Suspense>
</SuspenseList>
);
}