From 0e253456a58961304ec00ae52433d1e6062ed5bb Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 8 Feb 2024 16:35:14 -0500 Subject: [PATCH] Throw a better error when Lazy/Promise is used in React.Children --- packages/react/src/ReactChildren.js | 14 ++++++++++++ .../react/src/__tests__/ReactChildren-test.js | 22 +++++++++++++++++++ scripts/error-codes/codes.json | 3 ++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/react/src/ReactChildren.js b/packages/react/src/ReactChildren.js index 2bbd980046eb3..2632e2edb0708 100644 --- a/packages/react/src/ReactChildren.js +++ b/packages/react/src/ReactChildren.js @@ -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'; @@ -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.' + ); } } } @@ -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]' diff --git a/packages/react/src/__tests__/ReactChildren-test.js b/packages/react/src/__tests__/ReactChildren-test.js index 4a3898e4bd8ce..4c64117e215e4 100644 --- a/packages/react/src/__tests__/ReactChildren-test.js +++ b/packages/react/src/__tests__/ReactChildren-test.js @@ -948,6 +948,28 @@ describe('ReactChildren', () => { ); }); + it('should throw on React.lazy', async () => { + const lazyElement = React.lazy(async () => ({default:
})); + 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(
); + 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: diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index d02f0f6e1d4ec..657c9005a57bf 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -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 inside React.Children. We recommend not iterating over children and just rendering them plain." }