Skip to content

Commit

Permalink
Warn for Child Iterator of all types but allow Generator Components (#…
Browse files Browse the repository at this point in the history
…28853)

This doesn't change production behavior. We always render Iterables to
our best effort in prod even if they're Iterators.

But this does change the DEV warnings which indicates which are valid
patterns to use.

It's a footgun to use an Iterator as a prop when you pass between
components because if an intermediate component rerenders without its
parent, React won't be able to iterate it again to reconcile and any
mappers won't be able to re-apply. This is actually typically not a
problem when passed only to React host components but as a pattern it's
a problem for composability.

We used to warn only for Generators - i.e. Iterators returned from
Generator functions. This adds a warning for Iterators created by other
means too (e.g. Flight or the native Iterator utils). The heuristic is
to check whether the Iterator is the same as the Iterable because that
means it's not possible to get new iterators out of it. This case used
to just yield non-sense like empty sets in DEV but not in prod.

However, a new realization is that when the Component itself is a
Generator Function, it's not actually a problem. That's because the
React Element itself works as an Iterable since we can ask for new
generators by calling the function again. So this adds a special case to
allow the Generator returned from a Generator Function's direct child.
The principle is “don’t pass iterators around” but in this case there is
no iterator floating around because it’s between React and the JS VM.

Also see #28849 for context on AsyncIterables.

Related to this, but Hooks should ideally be banned in these for the
same reason they're banned in Async Functions.

DiffTrain build for commit 3682021.
  • Loading branch information
sebmarkbage committed Apr 21, 2024
1 parent 0db1205 commit a77e608
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<8514bdf3885188125eab2e51d9ecb536>>
* @generated SignedSource<<fbc5f0d556063e18e8065897a4edae21>>
*/

'use strict';
Expand Down Expand Up @@ -6141,45 +6141,36 @@ function createChildReconciler(shouldTrackSideEffects) {
throw new Error('An object is not an iterable. This error is likely caused by a bug in ' + 'React. Please file an issue.');
}

{
// We don't support rendering Generators because it's a mutation.
// See https://github.com/facebook/react/issues/12995
if (typeof Symbol === 'function' && // $FlowFixMe[prop-missing] Flow doesn't know about toStringTag
newChildrenIterable[Symbol.toStringTag] === 'Generator') {
if (!didWarnAboutGenerators) {
error('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
var newChildren = iteratorFn.call(newChildrenIterable);

{
if (newChildren === newChildrenIterable) {
// We don't support rendering Generators as props because it's a mutation.
// See https://github.com/facebook/react/issues/12995
// We do support generators if they were created by a GeneratorFunction component
// as its direct child since we can recreate those by rerendering the component
// as needed.
var isGeneratorComponent = returnFiber.tag === FunctionComponent && // $FlowFixMe[method-unbinding]
Object.prototype.toString.call(returnFiber.type) === '[object GeneratorFunction]' && // $FlowFixMe[method-unbinding]
Object.prototype.toString.call(newChildren) === '[object Generator]';

if (!isGeneratorComponent) {
if (!didWarnAboutGenerators) {
error('Using Iterators 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. You can also use an ' + 'Iterable that can iterate multiple times over the same items.');
}

if (newChildrenIterable.entries === iteratorFn) {
didWarnAboutGenerators = true;
}
} else if (newChildrenIterable.entries === iteratorFn) {
// Warn about using Maps as children
if (!didWarnAboutMaps) {
error('Using Maps as children is not supported. ' + 'Use an array of keyed ReactElements instead.');
}

didWarnAboutMaps = true;
} // First, validate keys.
// We'll get a different iterator later for the main pass.


var _newChildren = iteratorFn.call(newChildrenIterable);

if (_newChildren) {
var knownKeys = null;

var _step = _newChildren.next();

for (; !_step.done; _step = _newChildren.next()) {
var child = _step.value;
knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
didWarnAboutMaps = true;
}
}
}

var newChildren = iteratorFn.call(newChildrenIterable);

if (newChildren == null) {
throw new Error('An iterable object provided no iterator.');
}
Expand All @@ -6190,9 +6181,14 @@ function createChildReconciler(shouldTrackSideEffects) {
var lastPlacedIndex = 0;
var newIdx = 0;
var nextOldFiber = null;
var knownKeys = null;
var step = newChildren.next();

for (; oldFiber !== null && !step.done; newIdx++, step = newChildren.next()) {
{
knownKeys = warnOnInvalidKey(step.value, knownKeys, returnFiber);
}

for (; oldFiber !== null && !step.done; newIdx++, step = newChildren.next(), knownKeys = warnOnInvalidKey(step.value, knownKeys, returnFiber) ) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
Expand Down Expand Up @@ -6249,7 +6245,7 @@ function createChildReconciler(shouldTrackSideEffects) {
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; !step.done; newIdx++, step = newChildren.next()) {
for (; !step.done; newIdx++, step = newChildren.next(), knownKeys = warnOnInvalidKey(step.value, knownKeys, returnFiber) ) {
var _newFiber3 = createChild(returnFiber, step.value, lanes, debugInfo);

if (_newFiber3 === null) {
Expand All @@ -6274,7 +6270,7 @@ function createChildReconciler(shouldTrackSideEffects) {

var existingChildren = mapRemainingChildren(oldFiber); // Keep scanning and use the map to restore deleted items as moves.

for (; !step.done; newIdx++, step = newChildren.next()) {
for (; !step.done; newIdx++, step = newChildren.next(), knownKeys = warnOnInvalidKey(step.value, knownKeys, returnFiber) ) {
var _newFiber4 = updateFromMap(existingChildren, returnFiber, newIdx, step.value, lanes, debugInfo);

if (_newFiber4 !== null) {
Expand Down Expand Up @@ -22990,7 +22986,7 @@ identifierPrefix, onUncaughtError, onCaughtError, onRecoverableError, transition
return root;
}

var ReactVersion = '19.0.0-canary-66405eaa';
var ReactVersion = '19.0.0-canary-c4e3bea9';

/*
* The `'' + value` pattern (used in perf-sensitive code) throws for Symbol
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<f3eb1768c908f25292e8f079ec672f53>>
* @generated SignedSource<<8230647a80c4b86b8cf8384768883557>>
*/

"use strict";
Expand Down Expand Up @@ -1825,7 +1825,7 @@ function createChildReconciler(shouldTrackSideEffects) {
nextOldFiber = null,
step = newChildrenIterable.next();
null !== oldFiber && !step.done;
newIdx++, step = newChildrenIterable.next()
newIdx++, step = newChildrenIterable.next(), null
) {
oldFiber.index > newIdx
? ((nextOldFiber = oldFiber), (oldFiber = null))
Expand All @@ -1849,7 +1849,7 @@ function createChildReconciler(shouldTrackSideEffects) {
if (step.done)
return deleteRemainingChildren(returnFiber, oldFiber), iteratorFn;
if (null === oldFiber) {
for (; !step.done; newIdx++, step = newChildrenIterable.next())
for (; !step.done; newIdx++, step = newChildrenIterable.next(), null)
(step = createChild(returnFiber, step.value, lanes)),
null !== step &&
((currentFirstChild = placeChild(step, currentFirstChild, newIdx)),
Expand All @@ -1862,7 +1862,7 @@ function createChildReconciler(shouldTrackSideEffects) {
for (
oldFiber = mapRemainingChildren(oldFiber);
!step.done;
newIdx++, step = newChildrenIterable.next()
newIdx++, step = newChildrenIterable.next(), null
)
(step = updateFromMap(oldFiber, returnFiber, newIdx, step.value, lanes)),
null !== step &&
Expand Down Expand Up @@ -9144,7 +9144,7 @@ var devToolsConfig$jscomp$inline_1019 = {
throw Error("TestRenderer does not support findFiberByHostInstance()");
},
bundleType: 0,
version: "19.0.0-canary-7a48fce4",
version: "19.0.0-canary-a8200c0a",
rendererPackageName: "react-test-renderer"
};
var internals$jscomp$inline_1238 = {
Expand Down Expand Up @@ -9175,7 +9175,7 @@ var internals$jscomp$inline_1238 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "19.0.0-canary-7a48fce4"
reconcilerVersion: "19.0.0-canary-a8200c0a"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1239 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<9206ca0f6781a4c840d6d393f26b3429>>
* @generated SignedSource<<9f8b5b89043962a79b6b2123a526bf46>>
*/

"use strict";
Expand Down Expand Up @@ -1913,7 +1913,7 @@ function createChildReconciler(shouldTrackSideEffects) {
nextOldFiber = null,
step = newChildrenIterable.next();
null !== oldFiber && !step.done;
newIdx++, step = newChildrenIterable.next()
newIdx++, step = newChildrenIterable.next(), null
) {
oldFiber.index > newIdx
? ((nextOldFiber = oldFiber), (oldFiber = null))
Expand All @@ -1937,7 +1937,7 @@ function createChildReconciler(shouldTrackSideEffects) {
if (step.done)
return deleteRemainingChildren(returnFiber, oldFiber), iteratorFn;
if (null === oldFiber) {
for (; !step.done; newIdx++, step = newChildrenIterable.next())
for (; !step.done; newIdx++, step = newChildrenIterable.next(), null)
(step = createChild(returnFiber, step.value, lanes)),
null !== step &&
((currentFirstChild = placeChild(step, currentFirstChild, newIdx)),
Expand All @@ -1950,7 +1950,7 @@ function createChildReconciler(shouldTrackSideEffects) {
for (
oldFiber = mapRemainingChildren(oldFiber);
!step.done;
newIdx++, step = newChildrenIterable.next()
newIdx++, step = newChildrenIterable.next(), null
)
(step = updateFromMap(oldFiber, returnFiber, newIdx, step.value, lanes)),
null !== step &&
Expand Down Expand Up @@ -9760,7 +9760,7 @@ var devToolsConfig$jscomp$inline_1101 = {
throw Error("TestRenderer does not support findFiberByHostInstance()");
},
bundleType: 0,
version: "19.0.0-canary-39a6938a",
version: "19.0.0-canary-359342b1",
rendererPackageName: "react-test-renderer"
};
(function (internals) {
Expand Down Expand Up @@ -9804,7 +9804,7 @@ var devToolsConfig$jscomp$inline_1101 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "19.0.0-canary-39a6938a"
reconcilerVersion: "19.0.0-canary-359342b1"
});
exports._Scheduler = Scheduler;
exports.act = act;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<8f878889d9995d2ae240b302c52058bd>>
* @generated SignedSource<<b7764b0ac63cb81257aa2e03fa2024ca>>
*/

'use strict';
Expand Down Expand Up @@ -1304,11 +1304,14 @@ function validateChildKeys(node, parentType) {
// but now we print a separate warning for them later.
if (iteratorFn !== node.entries) {
var iterator = iteratorFn.call(node);
var step;

while (!(step = iterator.next()).done) {
if (isValidElement(step.value)) {
validateExplicitKey(step.value, parentType);
if (iterator !== node) {
var step;

while (!(step = iterator.next()).done) {
if (isValidElement(step.value)) {
validateExplicitKey(step.value, parentType);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<63334719117bc7e81815150b27588000>>
* @generated SignedSource<<6786f79c50baa1d713a06b4ed12afb60>>
*/

'use strict';
Expand Down Expand Up @@ -1328,11 +1328,14 @@ function validateChildKeys(node, parentType) {
// but now we print a separate warning for them later.
if (iteratorFn !== node.entries) {
var iterator = iteratorFn.call(node);
var step;

while (!(step = iterator.next()).done) {
if (isValidElement(step.value)) {
validateExplicitKey(step.value, parentType);
if (iterator !== node) {
var step;

while (!(step = iterator.next()).done) {
if (isValidElement(step.value)) {
validateExplicitKey(step.value, parentType);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<1ccd58cd787da0b40cbf5543503953db>>
* @generated SignedSource<<745c28cba28ff3a4a3a391cfdfdd1c1b>>
*/

'use strict';
Expand All @@ -27,7 +27,7 @@ if (
}
var dynamicFlagsUntyped = require('ReactNativeInternalFeatureFlags');

var ReactVersion = '19.0.0-canary-c627bf44';
var ReactVersion = '19.0.0-canary-e8808e1d';

// ATTENTION
// When adding new symbols to this file,
Expand Down Expand Up @@ -1889,11 +1889,14 @@ function validateChildKeys(node, parentType) {
// but now we print a separate warning for them later.
if (iteratorFn !== node.entries) {
var iterator = iteratorFn.call(node);
var step;

while (!(step = iterator.next()).done) {
if (isValidElement(step.value)) {
validateExplicitKey(step.value, parentType);
if (iterator !== node) {
var step;

while (!(step = iterator.next()).done) {
if (isValidElement(step.value)) {
validateExplicitKey(step.value, parentType);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ea26e38e33bffeba1ecc42688d7e8cd7e0da1c02
368202181e772d411b2445930aea1edd9428b09b
Loading

0 comments on commit a77e608

Please sign in to comment.