@samitouri / QOS-React-2 / commits / 48f6594474

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 }