Add warning when single item or nested arrays are used with SuspenseList (#16094)
Sebastian Markbåge committed
Jul 10, 2019 at 11:07 UTC
48f6594474c449b3ed62fadedda0ffad5e3a807a
2 files changed
+149
-2
packages/react-reconciler/src/ReactFiberBeginWork.js
+68
-1
@@ -67,7 +67,7 @@ import shallowEqual from 'shared/shallowEqual';
67
import getComponentName from 'shared/getComponentName';
68
import ReactStrictModeWarnings from './ReactStrictModeWarnings';
69
import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent';
70
-import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
70
+import {REACT_LAZY_TYPE, getIteratorFn} from 'shared/ReactSymbols';
71
import warning from 'shared/warning';
72
import warningWithoutStack from 'shared/warningWithoutStack';
73
import {
@@ -2094,6 +2094,72 @@ function validateTailOptions(
2094
}
2095
}
2096
2097
+function validateSuspenseListNestedChild(childSlot: mixed, index: number) {
2098
+ if (__DEV__) {
2099
+ let isArray = Array.isArray(childSlot);
2100
+ let isIterable = !isArray && typeof getIteratorFn(childSlot) === 'function';
2101
+ if (isArray || isIterable) {
2102
+ let type = isArray ? 'array' : 'iterable';
2103
+ warning(
2104
+ false,
2105
+ 'A nested %s was passed to row #%s in <SuspenseList />. Wrap it in ' +
2106
+ 'an additional SuspenseList to configure its revealOrder: ' +
2107
+ '<SuspenseList revealOrder=...> ... ' +
2108
+ '<SuspenseList revealOrder=...>{%s}</SuspenseList> ... ' +
2109
+ '</SuspenseList>',
2110
+ type,
2111
+ index,
2112
+ type,
2113
+ );
2114
+ return false;
2115
+ }
2116
+ }
2117
+ return true;
2118
+}
2119
+
2120
+function validateSuspenseListChildren(
2121
+ children: mixed,
2122
+ revealOrder: SuspenseListRevealOrder,
2123
+) {
2124
+ if (__DEV__) {
2125
+ if (
2126
+ (revealOrder === 'forwards' || revealOrder === 'backwards') &&
2127
+ (children !== undefined && children !== null && children !== false)
2128
+ ) {
2129
+ if (Array.isArray(children)) {
2130
+ for (let i = 0; i < children.length; i++) {
2131
+ if (!validateSuspenseListNestedChild(children[i], i)) {
2132
+ return;
2133
+ }
2134
+ }
2135
+ } else {
2136
+ let iteratorFn = getIteratorFn(children);
2137
+ if (typeof iteratorFn === 'function') {
2138
+ const childrenIterator = iteratorFn.call(children);
2139
+ if (childrenIterator) {
2140
+ let step = childrenIterator.next();
2141
+ let i = 0;
2142
+ for (; !step.done; step = childrenIterator.next()) {
2143
+ if (!validateSuspenseListNestedChild(step.value, i)) {
2144
+ return;
2145
+ }
2146
+ i++;
2147
+ }
2148
+ }
2149
+ } else {
2150
+ warning(
2151
+ false,
2152
+ 'A single row was passed to a <SuspenseList revealOrder="%s" />. ' +
2153
+ 'This is not useful since it needs multiple rows. ' +
2154
+ 'Did you mean to pass multiple children or an array?',
2155
+ revealOrder,
2156
+ );
2157
+ }
2158
+ }
2159
+ }
2160
+ }
2161
+}
2162
+
2163
function initSuspenseListRenderState(
2164
workInProgress: Fiber,
2165
isBackwards: boolean,
@@ -2142,6 +2208,7 @@ function updateSuspenseListComponent(
2208
2209
validateRevealOrder(revealOrder);
2210
validateTailOptions(tailMode, revealOrder);
2211
+ validateSuspenseListChildren(newChildren, revealOrder);
2212
2213
reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
2214
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js
+81
-1
@@ -101,6 +101,85 @@ describe('ReactSuspenseList', () => {
101
]);
102
});
103
104
+ it('warns if a single element is passed to a "forwards" list', () => {
105
+ function Foo({children}) {
106
+ return <SuspenseList revealOrder="forwards">{children}</SuspenseList>;
107
+ }
108
+
109
+ ReactNoop.render(<Foo />);
110
+ // No warning
111
+ Scheduler.unstable_flushAll();
112
+
113
+ ReactNoop.render(<Foo>{null}</Foo>);
114
+ // No warning
115
+ Scheduler.unstable_flushAll();
116
+
117
+ ReactNoop.render(<Foo>{false}</Foo>);
118
+ // No warning
119
+ Scheduler.unstable_flushAll();
120
+
121
+ ReactNoop.render(
122
+ <Foo>
123
+ <Suspense fallback="Loading">Child</Suspense>
124
+ </Foo>,
125
+ );
126
+
127
+ expect(() => Scheduler.unstable_flushAll()).toWarnDev([
128
+ 'Warning: A single row was passed to a <SuspenseList revealOrder="forwards" />. ' +
129
+ 'This is not useful since it needs multiple rows. ' +
130
+ 'Did you mean to pass multiple children or an array?' +
131
+ '\n in SuspenseList (at **)' +
132
+ '\n in Foo (at **)',
133
+ ]);
134
+ });
135
+
136
+ it('warns if a single fragment is passed to a "backwards" list', () => {
137
+ function Foo() {
138
+ return (
139
+ <SuspenseList revealOrder="backwards">
140
+ <Fragment>{[]}</Fragment>
141
+ </SuspenseList>
142
+ );
143
+ }
144
+
145
+ ReactNoop.render(<Foo />);
146
+
147
+ expect(() => Scheduler.unstable_flushAll()).toWarnDev([
148
+ 'Warning: A single row was passed to a <SuspenseList revealOrder="backwards" />. ' +
149
+ 'This is not useful since it needs multiple rows. ' +
150
+ 'Did you mean to pass multiple children or an array?' +
151
+ '\n in SuspenseList (at **)' +
152
+ '\n in Foo (at **)',
153
+ ]);
154
+ });
155
+
156
+ it('warns if a nested array is passed to a "forwards" list', () => {
157
+ function Foo({items}) {
158
+ return (
159
+ <SuspenseList revealOrder="forwards">
160
+ {items.map(name => (
161
+ <Suspense key={name} fallback="Loading">
162
+ {name}
163
+ </Suspense>
164
+ ))}
165
+ <div>Tail</div>
166
+ </SuspenseList>
167
+ );
168
+ }
169
+
170
+ ReactNoop.render(<Foo items={['A', 'B']} />);
171
+
172
+ expect(() => Scheduler.unstable_flushAll()).toWarnDev([
173
+ 'Warning: A nested array was passed to row #0 in <SuspenseList />. ' +
174
+ 'Wrap it in an additional SuspenseList to configure its revealOrder: ' +
175
+ '<SuspenseList revealOrder=...> ... ' +
176
+ '<SuspenseList revealOrder=...>{array}</SuspenseList> ... ' +
177
+ '</SuspenseList>' +
178
+ '\n in SuspenseList (at **)' +
179
+ '\n in Foo (at **)',
180
+ ]);
181
+ });
182
+
183
it('shows content independently by default', async () => {
184
let A = createAsyncText('A');
185
let B = createAsyncText('B');
@@ -1162,7 +1241,8 @@ describe('ReactSuspenseList', () => {
1241
function Foo() {
1242
return (
1243
<SuspenseList revealOrder="forwards" tail="collapse">
1165
- <Suspense fallback="Loading">Content</Suspense>
1244
+ <Suspense fallback="Loading">A</Suspense>
1245
+ <Suspense fallback="Loading">B</Suspense>
1246
</SuspenseList>
1247
);
1248
}