diff --git a/packages/react/src/ReactChildren.js b/packages/react/src/ReactChildren.js index 391c0985ef85e..7296a452c00ad 100644 --- a/packages/react/src/ReactChildren.js +++ b/packages/react/src/ReactChildren.js @@ -229,11 +229,21 @@ function mapIntoArray( childKey, ); if (__DEV__) { - if (nameSoFar !== '' && mappedChild.key == null) { - // We need to validate that this child should have had a key before assigning it one. - if (!newChild._store.validated) { - // We mark this child as having failed validation but we let the actual renderer - // print the warning later. + // If `child` was an element without a `key`, we need to validate if + // it should have had a `key`, before assigning one to `mappedChild`. + // $FlowFixMe[incompatible-type] Flow incorrectly thinks React.Portal doesn't have a key + if ( + nameSoFar !== '' && + child != null && + isValidElement(child) && + child.key == null + ) { + // We check truthiness of `child._store.validated` instead of being + // inequal to `1` to provide a bit of backward compatibility for any + // libraries (like `fbt`) which may be hacking this property. + if (child._store && !child._store.validated) { + // Mark this child as having failed validation, but let the actual + // renderer print the warning later. newChild._store.validated = 2; } } diff --git a/packages/react/src/__tests__/ReactChildren-test.js b/packages/react/src/__tests__/ReactChildren-test.js index 6a97465d3d97f..c4e92c44cccb0 100644 --- a/packages/react/src/__tests__/ReactChildren-test.js +++ b/packages/react/src/__tests__/ReactChildren-test.js @@ -868,7 +868,105 @@ describe('ReactChildren', () => { ]); }); - it('should warn for flattened children lists', async () => { + it('warns for mapped list children without keys', async () => { + function ComponentRenderingMappedChildren({children}) { + return ( +
+ {React.Children.map(children, child => ( +
+ ))} +
+ ); + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render( + + {[
]} + , + ); + }); + }).toErrorDev([ + 'Warning: Each child in a list should have a unique "key" prop.', + ]); + }); + + it('does not warn for mapped static children without keys', async () => { + function ComponentRenderingMappedChildren({children}) { + return ( +
+ {React.Children.map(children, child => ( +
+ ))} +
+ ); + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render( + +
+
+ , + ); + }); + }).toErrorDev([]); + }); + + it('warns for cloned list children without keys', async () => { + function ComponentRenderingClonedChildren({children}) { + return ( +
+ {React.Children.map(children, child => React.cloneElement(child))} +
+ ); + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render( + + {[
]} + , + ); + }); + }).toErrorDev([ + 'Warning: Each child in a list should have a unique "key" prop.', + ]); + }); + + it('does not warn for cloned static children without keys', async () => { + function ComponentRenderingClonedChildren({children}) { + return ( +
+ {React.Children.map(children, child => React.cloneElement(child))} +
+ ); + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render( + +
+
+ , + ); + }); + }).toErrorDev([]); + }); + + it('warns for flattened list children without keys', async () => { function ComponentRenderingFlattenedChildren({children}) { return
{React.Children.toArray(children)}
; } @@ -888,7 +986,7 @@ describe('ReactChildren', () => { ]); }); - it('does not warn for flattened positional children', async () => { + it('does not warn for flattened static children without keys', async () => { function ComponentRenderingFlattenedChildren({children}) { return
{React.Children.toArray(children)}
; }