Throw a better error when Lazy/Promise is used in React.Children (#28280)
We could in theory actually support this case by throwing a Promise when it's used inside a render. Allowing it to be synchronously unwrapped. However, it's a bit sketchy because we officially only support this in the render's child position or in `use()`. Another alternative could be to actually pass the Promise/Lazy to the callback so that you can reason about it and just return it again or even unwrapping with `use()` - at least for the forEach case maybe.
Sebastian Markbåge committed
Feb 8, 2024 at 14:10 UTC
e41ee9ea70f8998144fdd959ac11fd7a40e4ee20
3 files changed
+38
-1
packages/react/src/ReactChildren.js
+14
@@ -13,6 +13,7 @@ import isArray from 'shared/isArray';
13
import {
14
getIteratorFn,
15
REACT_ELEMENT_TYPE,
16
+ REACT_LAZY_TYPE,
17
REACT_PORTAL_TYPE,
18
} from 'shared/ReactSymbols';
19
import {checkKeyStringCoercion} from 'shared/CheckStringCoercion';
@@ -103,6 +104,12 @@ function mapIntoArray(
104
case REACT_ELEMENT_TYPE:
105
case REACT_PORTAL_TYPE:
106
invokeCallback = true;
107
+ break;
108
+ case REACT_LAZY_TYPE:
109
+ throw new Error(
110
+ 'Cannot render an Async Component, Promise or React.Lazy inside React.Children. ' +
111
+ 'We recommend not iterating over children and just rendering them plain.',
112
+ );
113
}
114
}
115
}
@@ -207,6 +214,13 @@ function mapIntoArray(
214
// eslint-disable-next-line react-internal/safe-string-coercion
215
const childrenString = String((children: any));
216
217
+ if (typeof (children: any).then === 'function') {
218
+ throw new Error(
219
+ 'Cannot render an Async Component, Promise or React.Lazy inside React.Children. ' +
220
+ 'We recommend not iterating over children and just rendering them plain.',
221
+ );
222
+ }
223
+
224
throw new Error(
225
`Objects are not valid as a React child (found: ${
226
childrenString === '[object Object]'
packages/react/src/__tests__/ReactChildren-test.js
+22
@@ -948,6 +948,28 @@ describe('ReactChildren', () => {
948
);
949
});
950
951
+ it('should throw on React.lazy', async () => {
952
+ const lazyElement = React.lazy(async () => ({default: <div />}));
953
+ await expect(() => {
954
+ React.Children.forEach([lazyElement], () => {}, null);
955
+ }).toThrowError(
956
+ 'Cannot render an Async Component, Promise or React.Lazy inside React.Children. ' +
957
+ 'We recommend not iterating over children and just rendering them plain.',
958
+ {withoutStack: true}, // There's nothing on the stack
959
+ );
960
+ });
961
+
962
+ it('should throw on Promises', async () => {
963
+ const promise = Promise.resolve(<div />);
964
+ await expect(() => {
965
+ React.Children.forEach([promise], () => {}, null);
966
+ }).toThrowError(
967
+ 'Cannot render an Async Component, Promise or React.Lazy inside React.Children. ' +
968
+ 'We recommend not iterating over children and just rendering them plain.',
969
+ {withoutStack: true}, // There's nothing on the stack
970
+ );
971
+ });
972
+
973
it('should throw on regex', () => {
974
// Really, we care about dates (#4840) but those have nondeterministic
975
// serialization (timezones) so let's test a regex instead:
scripts/error-codes/codes.json
+2
-1
@@ -489,5 +489,6 @@
489
"501": "The render was aborted with postpone when the shell is incomplete. Reason: %s",
490
"502": "Cannot read a Client Context from a Server Component.",
491
"503": "Cannot use() an already resolved Client Reference.",
492
- "504": "Failed to read a RSC payload created by a development version of React on the server while using a production version on the client. Always use matching versions on the server and the client."
492
+ "504": "Failed to read a RSC payload created by a development version of React on the server while using a production version on the client. Always use matching versions on the server and the client.",
493
+ "505": "Cannot render an Async Component, Promise or React.Lazy inside React.Children. We recommend not iterating over children and just rendering them plain."
494
}