@samitouri / QOS-React / commits / e41ee9ea70

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 }