Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Missing key Validation in React.Children #29675

Merged
merged 4 commits into from
May 31, 2024

Conversation

yungsters
Copy link
Contributor

@yungsters yungsters commented May 30, 2024

Summary

In #29088, the validation logic for React.Children inspected whether mappedChild — the return value of the map callback — has a valid key. However, this deviates from existing behavior which only warns if the original child is missing a required key.

This fixes false positive key validation warnings when using React.Children, by validating the original child instead of mappedChild.

This is a more general fix that expands upon my previous fix in #29662.

How did you test this change?

$ yarn test ReactChildren-test.js

Copy link

vercel bot commented May 30, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-compiler-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 31, 2024 0:58am

Copy link
Contributor Author

@yungsters yungsters left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, this does not include my most recent changes. Silly me…

return (
<div>
{React.Children.map(children, child => (
<div />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React actually does not care if mappedChild is missing a key.

await act(() => {
root.render(
<ComponentRenderingMappedChildren>
{[<div />]}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, React does care if the supplied child is missing a key (and is in a list).

Comment on lines +914 to +915
<div />
<div />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React is content with static child, even if mappedChild does not have a key.

function ComponentRenderingClonedChildren({children}) {
return (
<div>
{React.Children.map(children, child => React.cloneElement(child))}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the use case that I originally sought to fix.

I suppose this is now more or less the same as the test case above (that simply returns <div />), but maybe still worth keeping as a test case?

Seeking input on whether to keep this and the test case below, or to delete them.

@@ -888,7 +988,7 @@ describe('ReactChildren', () => {
]);
});

it('does not warn for flattened positional children', async () => {
it('does not warn for flattened static children without keys', async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized while reading more code that we refer to these as "static" children, so this is just a rename.

@@ -953,7 +953,7 @@ export function createElement(type, config, children) {
}

export function cloneAndReplaceKey(oldElement, newKey) {
const clonedElement = ReactElement(
return ReactElement(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a revert of #29662, which is no longer necessary.

@react-sizebot
Copy link

react-sizebot commented May 30, 2024

Comparing: 9710853...5d26f17

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.66 kB 6.66 kB = 1.82 kB 1.82 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 496.39 kB 496.39 kB = 88.84 kB 88.84 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.67 kB 6.67 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 501.21 kB 501.21 kB = 89.54 kB 89.54 kB
facebook-www/ReactDOM-prod.classic.js = 593.88 kB 593.88 kB = 104.46 kB 104.46 kB
facebook-www/ReactDOM-prod.modern.js = 570.26 kB 570.26 kB = 100.87 kB 100.87 kB
test_utils/ReactAllWarnings.js Deleted 63.65 kB 0.00 kB Deleted 15.90 kB 0.00 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react/cjs/react.react-server.development.js +0.76% 58.59 kB 59.03 kB +0.74% 16.69 kB 16.81 kB
oss-stable-semver/react/cjs/react.react-server.development.js +0.64% 69.70 kB 70.14 kB +0.65% 19.52 kB 19.65 kB
oss-stable/react/cjs/react.react-server.development.js +0.64% 69.72 kB 70.16 kB +0.64% 19.55 kB 19.67 kB
oss-experimental/react/cjs/react.development.js +0.56% 78.88 kB 79.32 kB +0.58% 21.71 kB 21.84 kB
oss-stable-semver/react/cjs/react.development.js +0.47% 94.09 kB 94.54 kB +0.48% 25.81 kB 25.94 kB
oss-stable/react/cjs/react.development.js +0.47% 94.12 kB 94.56 kB +0.48% 25.84 kB 25.97 kB
facebook-react-native/react/cjs/React-dev.js +0.42% 105.05 kB 105.50 kB +0.49% 28.22 kB 28.36 kB
facebook-www/React-dev.modern.js +0.38% 116.10 kB 116.55 kB +0.44% 30.13 kB 30.27 kB
facebook-www/React-dev.classic.js +0.38% 116.59 kB 117.04 kB +0.42% 30.23 kB 30.36 kB
test_utils/ReactAllWarnings.js Deleted 63.65 kB 0.00 kB Deleted 15.90 kB 0.00 kB

Generated by 🚫 dangerJS against 5d26f17

@@ -966,11 +966,6 @@ export function cloneAndReplaceKey(oldElement, newKey) {
__DEV__ && enableOwnerStacks ? oldElement._debugStack : undefined,
__DEV__ && enableOwnerStacks ? oldElement._debugTask : undefined,
);
if (__DEV__) {
// The cloned element should inherit the original element's key validation.
clonedElement._store.validated = oldElement._store.validated;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the original element was validated === 2 because it came from Flight or another Children helper, then was passed through map again, then it turns into validated === 0 but since it does have a key now it wouldn't issue an error even though it should've.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I started tackling this, I was also considering copying this logic to React.cloneElement.

I'll revert this change for now, but I mention it to ask whether you think React.cloneElement should also persist the original element's _store.validated (and whether that is affected by a new key being supplied to React.cloneElement).

If you think there's something to do there, I'd be happy to follow up.

// 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is weird that React.Children doesn't operate on portals. They don't get assigned keys.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I copied this from the $FlowFixMe comments in the lines above. 😅

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know but I found it interesting that 1) we didn't refine the type above 2) that we're checking isValidElement at all.

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the revert is actually not correct now. We should probably keep that to transfer any unissued "forced" warning to happen later. Left a comment.

The other changes looks right though. Interesting that the root gets assigned a key.

@yungsters
Copy link
Contributor Author

Thanks for the quick review!

if (
nameSoFar !== '' &&
child != null &&
isValidElement(child) &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I'm looking closer, it seems like we're extracting a key from Portals with getElementKey.

I believe that this check should really check if is element or is portal.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess Portals don't have stores and they don't run the validation in ChildFiber so this is not necessary right now, and it's probably not worth adding key validation for portals after the fact.

@yungsters yungsters merged commit 8fd963a into facebook:main May 31, 2024
39 of 40 checks passed
@yungsters yungsters deleted the key-warnings branch May 31, 2024 01:02
github-actions bot pushed a commit that referenced this pull request May 31, 2024
## Summary

In #29088, the validation logic
for `React.Children` inspected whether `mappedChild` — the return value
of the map callback — has a valid `key`. However, this deviates from
existing behavior which only warns if the original `child` is missing a
required `key`.

This fixes false positive `key` validation warnings when using
`React.Children`, by validating the original `child` instead of
`mappedChild`.

This is a more general fix that expands upon my previous fix in
#29662.

## How did you test this change?

```
$ yarn test ReactChildren-test.js
```

DiffTrain build for commit 8fd963a.
github-actions bot pushed a commit that referenced this pull request May 31, 2024
## Summary

In #29088, the validation logic
for `React.Children` inspected whether `mappedChild` — the return value
of the map callback — has a valid `key`. However, this deviates from
existing behavior which only warns if the original `child` is missing a
required `key`.

This fixes false positive `key` validation warnings when using
`React.Children`, by validating the original `child` instead of
`mappedChild`.

This is a more general fix that expands upon my previous fix in
#29662.

## How did you test this change?

```
$ yarn test ReactChildren-test.js
```

DiffTrain build for [8fd963a](8fd963a)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants