diff --git a/packages/react-dom/src/__tests__/ReactMultiChild-test.js b/packages/react-dom/src/__tests__/ReactMultiChild-test.js
index 906934cfa66be..e1d4d87ea75ee 100644
--- a/packages/react-dom/src/__tests__/ReactMultiChild-test.js
+++ b/packages/react-dom/src/__tests__/ReactMultiChild-test.js
@@ -294,6 +294,67 @@ describe('ReactMultiChild', () => {
);
});
+ it('should warn for using generators as children', () => {
+ function* Foo() {
+ yield
Hello
;
+ yield World
;
+ }
+
+ const div = document.createElement('div');
+ expect(() => {
+ ReactDOM.render(, div);
+ }).toWarnDev(
+ 'Using Generators as children is unsupported and will likely yield ' +
+ 'unexpected results because enumerating a generator mutates it. You may ' +
+ 'convert it to an array with `Array.from()` or the `[...spread]` operator ' +
+ 'before rendering. Keep in mind you might need to polyfill these features for older browsers.\n' +
+ ' in Foo (at **)',
+ );
+
+ // Test de-duplication
+ ReactDOM.render(, div);
+ });
+
+ it('should not warn for using generators in legacy iterables', () => {
+ const fooIterable = {
+ '@@iterator': function*() {
+ yield Hello
;
+ yield World
;
+ },
+ };
+
+ function Foo() {
+ return fooIterable;
+ }
+
+ const div = document.createElement('div');
+ ReactDOM.render(, div);
+ expect(div.textContent).toBe('HelloWorld');
+
+ ReactDOM.render(, div);
+ expect(div.textContent).toBe('HelloWorld');
+ });
+
+ it('should not warn for using generators in modern iterables', () => {
+ const fooIterable = {
+ [Symbol.iterator]: function*() {
+ yield Hello
;
+ yield World
;
+ },
+ };
+
+ function Foo() {
+ return fooIterable;
+ }
+
+ const div = document.createElement('div');
+ ReactDOM.render(, div);
+ expect(div.textContent).toBe('HelloWorld');
+
+ ReactDOM.render(, div);
+ expect(div.textContent).toBe('HelloWorld');
+ });
+
it('should reorder bailed-out children', () => {
class LetterInner extends React.Component {
render() {
diff --git a/packages/react-dom/src/__tests__/ReactMultiChildReconcile-test.js b/packages/react-dom/src/__tests__/ReactMultiChildReconcile-test.js
index 090230be3e7a2..f04b7ece7aab2 100644
--- a/packages/react-dom/src/__tests__/ReactMultiChildReconcile-test.js
+++ b/packages/react-dom/src/__tests__/ReactMultiChildReconcile-test.js
@@ -248,7 +248,7 @@ function prepareChildrenArray(childrenArray) {
return childrenArray;
}
-function prepareChildrenIterable(childrenArray) {
+function prepareChildrenLegacyIterable(childrenArray) {
return {
'@@iterator': function*() {
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
@@ -259,9 +259,27 @@ function prepareChildrenIterable(childrenArray) {
};
}
+function prepareChildrenModernIterable(childrenArray) {
+ return {
+ [Symbol.iterator]: function*() {
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
+ for (const child of childrenArray) {
+ yield child;
+ }
+ },
+ };
+}
+
function testPropsSequence(sequence) {
testPropsSequenceWithPreparedChildren(sequence, prepareChildrenArray);
- testPropsSequenceWithPreparedChildren(sequence, prepareChildrenIterable);
+ testPropsSequenceWithPreparedChildren(
+ sequence,
+ prepareChildrenLegacyIterable,
+ );
+ testPropsSequenceWithPreparedChildren(
+ sequence,
+ prepareChildrenModernIterable,
+ );
}
describe('ReactMultiChildReconcile', () => {
@@ -311,7 +329,49 @@ describe('ReactMultiChildReconcile', () => {
);
});
- it('should reset internal state if removed then readded in an iterable', () => {
+ it('should reset internal state if removed then readded in a legacy iterable', () => {
+ // Test basics.
+ const props = {
+ usernameToStatus: {
+ jcw: 'jcwStatus',
+ },
+ };
+
+ const container = document.createElement('div');
+ const parentInstance = ReactDOM.render(
+ ,
+ container,
+ );
+ let statusDisplays = parentInstance.getStatusDisplays();
+ const startingInternalState = statusDisplays.jcw.getInternalState();
+
+ // Now remove the child.
+ ReactDOM.render(
+ ,
+ container,
+ );
+ statusDisplays = parentInstance.getStatusDisplays();
+ expect(statusDisplays.jcw).toBeFalsy();
+
+ // Now reset the props that cause there to be a child
+ ReactDOM.render(
+ ,
+ container,
+ );
+ statusDisplays = parentInstance.getStatusDisplays();
+ expect(statusDisplays.jcw).toBeTruthy();
+ expect(statusDisplays.jcw.getInternalState()).not.toBe(
+ startingInternalState,
+ );
+ });
+
+ it('should reset internal state if removed then readded in a modern iterable', () => {
// Test basics.
const props = {
usernameToStatus: {
@@ -323,7 +383,7 @@ describe('ReactMultiChildReconcile', () => {
const parentInstance = ReactDOM.render(
,
container,
);
@@ -332,7 +392,7 @@ describe('ReactMultiChildReconcile', () => {
// Now remove the child.
ReactDOM.render(
- ,
+ ,
container,
);
statusDisplays = parentInstance.getStatusDisplays();
@@ -342,7 +402,7 @@ describe('ReactMultiChildReconcile', () => {
ReactDOM.render(
,
container,
);
diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js
index 97aaac8c2c8b4..1ebe9863ebc56 100644
--- a/packages/react-reconciler/src/ReactChildFiber.js
+++ b/packages/react-reconciler/src/ReactChildFiber.js
@@ -46,6 +46,7 @@ import {
import {StrictMode} from './ReactTypeOfMode';
let didWarnAboutMaps;
+let didWarnAboutGenerators;
let didWarnAboutStringRefInStrictMode;
let ownerHasKeyUseWarning;
let ownerHasFunctionTypeWarning;
@@ -53,6 +54,7 @@ let warnForMissingKey = (child: mixed) => {};
if (__DEV__) {
didWarnAboutMaps = false;
+ didWarnAboutGenerators = false;
didWarnAboutStringRefInStrictMode = {};
/**
@@ -903,6 +905,24 @@ function ChildReconciler(shouldTrackSideEffects) {
);
if (__DEV__) {
+ // We don't support rendering Generators because it's a mutation.
+ // See https://github.com/facebook/react/issues/12995
+ if (
+ typeof Symbol === 'function' &&
+ // $FlowFixMe Flow doesn't know about toStringTag
+ newChildrenIterable[Symbol.toStringTag] === 'Generator'
+ ) {
+ warning(
+ didWarnAboutGenerators,
+ 'Using Generators as children is unsupported and will likely yield ' +
+ 'unexpected results because enumerating a generator mutates it. ' +
+ 'You may convert it to an array with `Array.from()` or the ' +
+ '`[...spread]` operator before rendering. Keep in mind ' +
+ 'you might need to polyfill these features for older browsers.',
+ );
+ didWarnAboutGenerators = true;
+ }
+
// Warn about using Maps as children
if ((newChildrenIterable: any).entries === iteratorFn) {
warning(