Skip to content

Commit 1609cf3

Browse files
authored
Warn about rendering Generators (#13312)
* Warn about rendering Generators * Fix Flow * Add an explicit test for iterable * Moar test coverage
1 parent 46d5afc commit 1609cf3

File tree

3 files changed

+147
-6
lines changed

3 files changed

+147
-6
lines changed

packages/react-dom/src/__tests__/ReactMultiChild-test.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,67 @@ describe('ReactMultiChild', () => {
294294
);
295295
});
296296

297+
it('should warn for using generators as children', () => {
298+
function* Foo() {
299+
yield <h1 key="1">Hello</h1>;
300+
yield <h1 key="2">World</h1>;
301+
}
302+
303+
const div = document.createElement('div');
304+
expect(() => {
305+
ReactDOM.render(<Foo />, div);
306+
}).toWarnDev(
307+
'Using Generators as children is unsupported and will likely yield ' +
308+
'unexpected results because enumerating a generator mutates it. You may ' +
309+
'convert it to an array with `Array.from()` or the `[...spread]` operator ' +
310+
'before rendering. Keep in mind you might need to polyfill these features for older browsers.\n' +
311+
' in Foo (at **)',
312+
);
313+
314+
// Test de-duplication
315+
ReactDOM.render(<Foo />, div);
316+
});
317+
318+
it('should not warn for using generators in legacy iterables', () => {
319+
const fooIterable = {
320+
'@@iterator': function*() {
321+
yield <h1 key="1">Hello</h1>;
322+
yield <h1 key="2">World</h1>;
323+
},
324+
};
325+
326+
function Foo() {
327+
return fooIterable;
328+
}
329+
330+
const div = document.createElement('div');
331+
ReactDOM.render(<Foo />, div);
332+
expect(div.textContent).toBe('HelloWorld');
333+
334+
ReactDOM.render(<Foo />, div);
335+
expect(div.textContent).toBe('HelloWorld');
336+
});
337+
338+
it('should not warn for using generators in modern iterables', () => {
339+
const fooIterable = {
340+
[Symbol.iterator]: function*() {
341+
yield <h1 key="1">Hello</h1>;
342+
yield <h1 key="2">World</h1>;
343+
},
344+
};
345+
346+
function Foo() {
347+
return fooIterable;
348+
}
349+
350+
const div = document.createElement('div');
351+
ReactDOM.render(<Foo />, div);
352+
expect(div.textContent).toBe('HelloWorld');
353+
354+
ReactDOM.render(<Foo />, div);
355+
expect(div.textContent).toBe('HelloWorld');
356+
});
357+
297358
it('should reorder bailed-out children', () => {
298359
class LetterInner extends React.Component {
299360
render() {

packages/react-dom/src/__tests__/ReactMultiChildReconcile-test.js

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ function prepareChildrenArray(childrenArray) {
248248
return childrenArray;
249249
}
250250

251-
function prepareChildrenIterable(childrenArray) {
251+
function prepareChildrenLegacyIterable(childrenArray) {
252252
return {
253253
'@@iterator': function*() {
254254
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
@@ -259,9 +259,27 @@ function prepareChildrenIterable(childrenArray) {
259259
};
260260
}
261261

262+
function prepareChildrenModernIterable(childrenArray) {
263+
return {
264+
[Symbol.iterator]: function*() {
265+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
266+
for (const child of childrenArray) {
267+
yield child;
268+
}
269+
},
270+
};
271+
}
272+
262273
function testPropsSequence(sequence) {
263274
testPropsSequenceWithPreparedChildren(sequence, prepareChildrenArray);
264-
testPropsSequenceWithPreparedChildren(sequence, prepareChildrenIterable);
275+
testPropsSequenceWithPreparedChildren(
276+
sequence,
277+
prepareChildrenLegacyIterable,
278+
);
279+
testPropsSequenceWithPreparedChildren(
280+
sequence,
281+
prepareChildrenModernIterable,
282+
);
265283
}
266284

267285
describe('ReactMultiChildReconcile', () => {
@@ -311,7 +329,49 @@ describe('ReactMultiChildReconcile', () => {
311329
);
312330
});
313331

314-
it('should reset internal state if removed then readded in an iterable', () => {
332+
it('should reset internal state if removed then readded in a legacy iterable', () => {
333+
// Test basics.
334+
const props = {
335+
usernameToStatus: {
336+
jcw: 'jcwStatus',
337+
},
338+
};
339+
340+
const container = document.createElement('div');
341+
const parentInstance = ReactDOM.render(
342+
<FriendsStatusDisplay
343+
{...props}
344+
prepareChildren={prepareChildrenLegacyIterable}
345+
/>,
346+
container,
347+
);
348+
let statusDisplays = parentInstance.getStatusDisplays();
349+
const startingInternalState = statusDisplays.jcw.getInternalState();
350+
351+
// Now remove the child.
352+
ReactDOM.render(
353+
<FriendsStatusDisplay prepareChildren={prepareChildrenLegacyIterable} />,
354+
container,
355+
);
356+
statusDisplays = parentInstance.getStatusDisplays();
357+
expect(statusDisplays.jcw).toBeFalsy();
358+
359+
// Now reset the props that cause there to be a child
360+
ReactDOM.render(
361+
<FriendsStatusDisplay
362+
{...props}
363+
prepareChildren={prepareChildrenLegacyIterable}
364+
/>,
365+
container,
366+
);
367+
statusDisplays = parentInstance.getStatusDisplays();
368+
expect(statusDisplays.jcw).toBeTruthy();
369+
expect(statusDisplays.jcw.getInternalState()).not.toBe(
370+
startingInternalState,
371+
);
372+
});
373+
374+
it('should reset internal state if removed then readded in a modern iterable', () => {
315375
// Test basics.
316376
const props = {
317377
usernameToStatus: {
@@ -323,7 +383,7 @@ describe('ReactMultiChildReconcile', () => {
323383
const parentInstance = ReactDOM.render(
324384
<FriendsStatusDisplay
325385
{...props}
326-
prepareChildren={prepareChildrenIterable}
386+
prepareChildren={prepareChildrenModernIterable}
327387
/>,
328388
container,
329389
);
@@ -332,7 +392,7 @@ describe('ReactMultiChildReconcile', () => {
332392

333393
// Now remove the child.
334394
ReactDOM.render(
335-
<FriendsStatusDisplay prepareChildren={prepareChildrenIterable} />,
395+
<FriendsStatusDisplay prepareChildren={prepareChildrenModernIterable} />,
336396
container,
337397
);
338398
statusDisplays = parentInstance.getStatusDisplays();
@@ -342,7 +402,7 @@ describe('ReactMultiChildReconcile', () => {
342402
ReactDOM.render(
343403
<FriendsStatusDisplay
344404
{...props}
345-
prepareChildren={prepareChildrenIterable}
405+
prepareChildren={prepareChildrenModernIterable}
346406
/>,
347407
container,
348408
);

packages/react-reconciler/src/ReactChildFiber.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,15 @@ import {
4646
import {StrictMode} from './ReactTypeOfMode';
4747

4848
let didWarnAboutMaps;
49+
let didWarnAboutGenerators;
4950
let didWarnAboutStringRefInStrictMode;
5051
let ownerHasKeyUseWarning;
5152
let ownerHasFunctionTypeWarning;
5253
let warnForMissingKey = (child: mixed) => {};
5354

5455
if (__DEV__) {
5556
didWarnAboutMaps = false;
57+
didWarnAboutGenerators = false;
5658
didWarnAboutStringRefInStrictMode = {};
5759

5860
/**
@@ -903,6 +905,24 @@ function ChildReconciler(shouldTrackSideEffects) {
903905
);
904906

905907
if (__DEV__) {
908+
// We don't support rendering Generators because it's a mutation.
909+
// See https://github.com/facebook/react/issues/12995
910+
if (
911+
typeof Symbol === 'function' &&
912+
// $FlowFixMe Flow doesn't know about toStringTag
913+
newChildrenIterable[Symbol.toStringTag] === 'Generator'
914+
) {
915+
warning(
916+
didWarnAboutGenerators,
917+
'Using Generators as children is unsupported and will likely yield ' +
918+
'unexpected results because enumerating a generator mutates it. ' +
919+
'You may convert it to an array with `Array.from()` or the ' +
920+
'`[...spread]` operator before rendering. Keep in mind ' +
921+
'you might need to polyfill these features for older browsers.',
922+
);
923+
didWarnAboutGenerators = true;
924+
}
925+
906926
// Warn about using Maps as children
907927
if ((newChildrenIterable: any).entries === iteratorFn) {
908928
warning(

0 commit comments

Comments
 (0)