Skip to content

Commit

Permalink
Throw a better error when Lazy/Promise is used in React.Children (#28280
Browse files Browse the repository at this point in the history
)

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.
  • Loading branch information
sebmarkbage authored Feb 8, 2024
1 parent cd63ef7 commit e41ee9e
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 1 deletion.
14 changes: 14 additions & 0 deletions packages/react/src/ReactChildren.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import isArray from 'shared/isArray';
import {
getIteratorFn,
REACT_ELEMENT_TYPE,
REACT_LAZY_TYPE,
REACT_PORTAL_TYPE,
} from 'shared/ReactSymbols';
import {checkKeyStringCoercion} from 'shared/CheckStringCoercion';
Expand Down Expand Up @@ -103,6 +104,12 @@ function mapIntoArray(
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
invokeCallback = true;
break;
case REACT_LAZY_TYPE:
throw new Error(
'Cannot render an Async Component, Promise or React.Lazy inside React.Children. ' +
'We recommend not iterating over children and just rendering them plain.',
);
}
}
}
Expand Down Expand Up @@ -207,6 +214,13 @@ function mapIntoArray(
// eslint-disable-next-line react-internal/safe-string-coercion
const childrenString = String((children: any));

if (typeof (children: any).then === 'function') {
throw new Error(
'Cannot render an Async Component, Promise or React.Lazy inside React.Children. ' +
'We recommend not iterating over children and just rendering them plain.',
);
}

throw new Error(
`Objects are not valid as a React child (found: ${
childrenString === '[object Object]'
Expand Down
22 changes: 22 additions & 0 deletions packages/react/src/__tests__/ReactChildren-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,28 @@ describe('ReactChildren', () => {
);
});

it('should throw on React.lazy', async () => {
const lazyElement = React.lazy(async () => ({default: <div />}));
await expect(() => {
React.Children.forEach([lazyElement], () => {}, null);
}).toThrowError(
'Cannot render an Async Component, Promise or React.Lazy inside React.Children. ' +
'We recommend not iterating over children and just rendering them plain.',
{withoutStack: true}, // There's nothing on the stack
);
});

it('should throw on Promises', async () => {
const promise = Promise.resolve(<div />);
await expect(() => {
React.Children.forEach([promise], () => {}, null);
}).toThrowError(
'Cannot render an Async Component, Promise or React.Lazy inside React.Children. ' +
'We recommend not iterating over children and just rendering them plain.',
{withoutStack: true}, // There's nothing on the stack
);
});

it('should throw on regex', () => {
// Really, we care about dates (#4840) but those have nondeterministic
// serialization (timezones) so let's test a regex instead:
Expand Down
3 changes: 2 additions & 1 deletion scripts/error-codes/codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -489,5 +489,6 @@
"501": "The render was aborted with postpone when the shell is incomplete. Reason: %s",
"502": "Cannot read a Client Context from a Server Component.",
"503": "Cannot use() an already resolved Client Reference.",
"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."
"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.",
"505": "Cannot render an Async Component, Promise or React.Lazy inside React.Children. We recommend not iterating over children and just rendering them plain."
}

0 comments on commit e41ee9e

Please sign in to comment.