Warn about rendering Generators (#13312)

* Warn about rendering Generators

* Fix Flow

* Add an explicit test for iterable

* Moar test coverage
This commit is contained in:
Dan Abramov
2018-08-02 20:04:03 +01:00
committed by GitHub
parent 46d5afc54d
commit 1609cf3432
3 changed files with 147 additions and 6 deletions
@@ -294,6 +294,67 @@ describe('ReactMultiChild', () => {
);
});
it('should warn for using generators as children', () => {
function* Foo() {
yield <h1 key="1">Hello</h1>;
yield <h1 key="2">World</h1>;
}
const div = document.createElement('div');
expect(() => {
ReactDOM.render(<Foo />, 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(<Foo />, div);
});
it('should not warn for using generators in legacy iterables', () => {
const fooIterable = {
'@@iterator': function*() {
yield <h1 key="1">Hello</h1>;
yield <h1 key="2">World</h1>;
},
};
function Foo() {
return fooIterable;
}
const div = document.createElement('div');
ReactDOM.render(<Foo />, div);
expect(div.textContent).toBe('HelloWorld');
ReactDOM.render(<Foo />, div);
expect(div.textContent).toBe('HelloWorld');
});
it('should not warn for using generators in modern iterables', () => {
const fooIterable = {
[Symbol.iterator]: function*() {
yield <h1 key="1">Hello</h1>;
yield <h1 key="2">World</h1>;
},
};
function Foo() {
return fooIterable;
}
const div = document.createElement('div');
ReactDOM.render(<Foo />, div);
expect(div.textContent).toBe('HelloWorld');
ReactDOM.render(<Foo />, div);
expect(div.textContent).toBe('HelloWorld');
});
it('should reorder bailed-out children', () => {
class LetterInner extends React.Component {
render() {
@@ -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,7 @@ 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: {
@@ -323,7 +341,7 @@ describe('ReactMultiChildReconcile', () => {
const parentInstance = ReactDOM.render(
<FriendsStatusDisplay
{...props}
prepareChildren={prepareChildrenIterable}
prepareChildren={prepareChildrenLegacyIterable}
/>,
container,
);
@@ -332,7 +350,7 @@ describe('ReactMultiChildReconcile', () => {
// Now remove the child.
ReactDOM.render(
<FriendsStatusDisplay prepareChildren={prepareChildrenIterable} />,
<FriendsStatusDisplay prepareChildren={prepareChildrenLegacyIterable} />,
container,
);
statusDisplays = parentInstance.getStatusDisplays();
@@ -342,7 +360,49 @@ describe('ReactMultiChildReconcile', () => {
ReactDOM.render(
<FriendsStatusDisplay
{...props}
prepareChildren={prepareChildrenIterable}
prepareChildren={prepareChildrenLegacyIterable}
/>,
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: {
jcw: 'jcwStatus',
},
};
const container = document.createElement('div');
const parentInstance = ReactDOM.render(
<FriendsStatusDisplay
{...props}
prepareChildren={prepareChildrenModernIterable}
/>,
container,
);
let statusDisplays = parentInstance.getStatusDisplays();
const startingInternalState = statusDisplays.jcw.getInternalState();
// Now remove the child.
ReactDOM.render(
<FriendsStatusDisplay prepareChildren={prepareChildrenModernIterable} />,
container,
);
statusDisplays = parentInstance.getStatusDisplays();
expect(statusDisplays.jcw).toBeFalsy();
// Now reset the props that cause there to be a child
ReactDOM.render(
<FriendsStatusDisplay
{...props}
prepareChildren={prepareChildrenModernIterable}
/>,
container,
);
+20
View File
@@ -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(