mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user