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

Use already extracted values instead of reading off props for controlled components #26596

Merged
merged 3 commits into from
Apr 11, 2023

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Apr 11, 2023

Since props.x is a possibly megamorphic access, it can be slow to access and trigger recompilation.

When we are looping over the props and pattern matching every key, anyway, we've already done this work. We can just reuse the same value by stashing it outside the loop in the stack.

This only makes sense for updates in diffInCommitPhase since otherwise we don't have the full set of props in that loop.

We also have to be careful not to skip over equal values since we need to extract them anyway.

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Apr 11, 2023
@react-sizebot
Copy link

react-sizebot commented Apr 11, 2023

Comparing: ac43bf6...79e4988

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.min.js +0.27% 164.61 kB 165.05 kB +0.52% 51.69 kB 51.96 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.29% 167.00 kB 167.49 kB +0.54% 52.31 kB 52.59 kB
facebook-www/ReactDOM-prod.classic.js +0.46% 565.24 kB 567.82 kB +0.65% 99.34 kB 99.98 kB
facebook-www/ReactDOM-prod.modern.js +0.46% 549.08 kB 551.63 kB +0.66% 96.66 kB 97.30 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
facebook-www/ReactDOMTesting-prod.classic.js +0.48% 579.50 kB 582.27 kB +0.64% 103.02 kB 103.67 kB
facebook-www/ReactDOM-prod.modern.js +0.46% 549.08 kB 551.63 kB +0.66% 96.66 kB 97.30 kB
facebook-www/ReactDOM-prod.classic.js +0.46% 565.24 kB 567.82 kB +0.65% 99.34 kB 99.98 kB
facebook-www/ReactDOMTesting-prod.modern.js +0.45% 565.62 kB 568.17 kB +0.64% 100.84 kB 101.49 kB
facebook-www/ReactDOM-profiling.modern.js +0.44% 579.50 kB 582.05 kB +0.64% 101.15 kB 101.80 kB
facebook-www/ReactDOM-profiling.classic.js +0.43% 595.72 kB 598.31 kB +0.62% 103.88 kB 104.52 kB
facebook-www/ReactDOM-dev.modern.js +0.36% 1,395.09 kB 1,400.15 kB +0.26% 301.03 kB 301.81 kB
facebook-www/ReactDOM-dev.classic.js +0.36% 1,422.89 kB 1,428.04 kB +0.26% 306.60 kB 307.39 kB
facebook-www/ReactDOMTesting-dev.modern.js +0.36% 1,413.49 kB 1,418.55 kB +0.26% 305.50 kB 306.29 kB
facebook-www/ReactDOMTesting-dev.classic.js +0.36% 1,441.29 kB 1,446.44 kB +0.26% 310.83 kB 311.64 kB
oss-experimental/react-dom/umd/react-dom.production.min.js +0.29% 166.91 kB 167.39 kB +0.60% 52.68 kB 53.00 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.29% 167.00 kB 167.49 kB +0.54% 52.31 kB 52.59 kB
oss-experimental/react-dom/cjs/react-dom.development.js +0.28% 1,267.32 kB 1,270.90 kB +0.27% 278.82 kB 279.56 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.min.js +0.28% 173.21 kB 173.70 kB +0.52% 54.63 kB 54.91 kB
oss-experimental/react-dom/umd/react-dom.development.js +0.28% 1,328.77 kB 1,332.52 kB +0.23% 281.56 kB 282.21 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.development.js +0.28% 1,285.43 kB 1,289.01 kB +0.26% 283.21 kB 283.94 kB
oss-experimental/react-dom/umd/react-dom.profiling.min.js +0.28% 175.89 kB 176.38 kB +0.55% 54.99 kB 55.29 kB
oss-experimental/react-dom/cjs/react-dom.profiling.min.js +0.28% 176.63 kB 177.12 kB +0.52% 54.72 kB 55.01 kB
oss-stable-semver/react-dom/cjs/react-dom.production.min.js +0.27% 164.53 kB 164.98 kB +0.51% 51.67 kB 51.93 kB
oss-stable/react-dom/cjs/react-dom.production.min.js +0.27% 164.61 kB 165.05 kB +0.52% 51.69 kB 51.96 kB
oss-stable-semver/react-dom/umd/react-dom.production.min.js +0.27% 164.45 kB 164.89 kB +0.50% 52.10 kB 52.36 kB
oss-stable/react-dom/umd/react-dom.production.min.js +0.27% 164.52 kB 164.96 kB +0.49% 52.12 kB 52.38 kB
oss-stable-semver/react-dom/cjs/react-dom.profiling.min.js +0.25% 174.16 kB 174.61 kB +0.38% 54.13 kB 54.33 kB
oss-stable-semver/react-dom/umd/react-dom.profiling.min.js +0.25% 173.43 kB 173.87 kB +0.35% 54.41 kB 54.61 kB
oss-stable/react-dom/cjs/react-dom.profiling.min.js +0.25% 174.24 kB 174.68 kB +0.37% 54.15 kB 54.35 kB
oss-stable/react-dom/umd/react-dom.profiling.min.js +0.25% 173.50 kB 173.95 kB +0.36% 54.44 kB 54.63 kB
oss-stable-semver/react-dom/cjs/react-dom.development.js +0.21% 1,254.52 kB 1,257.21 kB +0.23% 276.80 kB 277.43 kB
oss-stable/react-dom/cjs/react-dom.development.js +0.21% 1,254.55 kB 1,257.23 kB +0.23% 276.82 kB 277.46 kB
oss-stable-semver/react-dom/umd/react-dom.development.js +0.21% 1,315.32 kB 1,318.12 kB +0.20% 279.69 kB 280.26 kB
oss-stable/react-dom/umd/react-dom.development.js +0.21% 1,315.34 kB 1,318.14 kB +0.20% 279.71 kB 280.28 kB

Generated by 🚫 dangerJS against 79e4988

Comment on lines +837 to +841
let type = null;
let value = null;
let defaultValue = null;
let checked = null;
let defaultChecked = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

you wrote these 5 variables in 3 different orders in the same function

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You mean in the argument order? The assignments are copy-paste.

(nextProp != null || lastProp != null)
) {
switch (propKey) {
case 'type': {
type = nextProp;
// Fast path since 'type' is very common on inputs
Copy link
Contributor

Choose a reason for hiding this comment

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

Changing 'type' is very uncommon though. setProp would be fewer bytes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I want to change this to domElement.type = nextProp but I leave that for a later refactor as well as dropping function and symbol as special cases which would then bring this down. For now I'm just highlighting that these special cases apply often. setProp would obscure that this should actually be simple.

Comment on lines +86 to +87
value: ?string,
defaultValue: ?string,
Copy link
Contributor

Choose a reason for hiding this comment

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

should these be mixed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes I mainly did this to ensure that Flow errors if you supply them out of order, which it doesn't really do anyway since the input is any most of the time.

Comment on lines +163 to +164
value: ?string,
defaultValue: ?string,
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto

if (isButton || value !== node.value) {
node.value = toString(value);
if (isButton || toString(getToStringValue(value)) !== node.value) {
node.value = toString(getToStringValue(value));
Copy link
Contributor

Choose a reason for hiding this comment

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

is it wasteful to stringify these many times?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yea but we already were in a bunch of place. I'm looking to get rid of these helpers in a follow up and align it more with how attributes do it.

@@ -203,8 +198,8 @@ export function initInput(
// prematurely marking required inputs as invalid. Equality is compared
// to the current value in case the browser provided value is not an
// empty string.
if (isButton || value !== node.value) {
node.value = toString(value);
if (isButton || toString(getToStringValue(value)) !== node.value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure if intentional but you added a toString() here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yea I think that's a bug fix.

@@ -264,9 +260,9 @@ export function initInput(
// Only assign the checked attribute if it is defined. This saves
// a DOM write when controlling the checked attribute isn't needed
// (text inputs, submit/reset)
if (props.defaultChecked != null) {
if (defaultChecked != null) {
node.defaultChecked = !node.defaultChecked;
Copy link
Contributor

Choose a reason for hiding this comment

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

(what does this extra assignment even do? seems like it should have no effect:

The checked content attribute is a boolean attribute that gives the default checkedness of the input element. When the checked content attribute is added, if the control does not have dirty checkedness, the user agent must set the checkedness of the element to true; when the checked content attribute is removed, if the control does not have dirty checkedness, the user agent must set the checkedness of the element to false.

)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Need investigation if it's a browser quirk. I could imagine browsers storing attribute and property values separately and missing a case where they should be updated and therefore not aligning. But not sure why this was added.

domElement.setAttribute('name', name);
} else {
domElement.removeAttribute('name');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

setProp here too? and/or is it faster to assign to .name which should be equivalent?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Same thing. I really just want to set domElement.name = name and I also want to make clear that setProp doesn't do anything complicated for this value.

@@ -1265,12 +1309,12 @@ export function updateProperties(
) {
switch (propKey) {
case 'checked': {
const checked = nextProps.defaultChecked;
Copy link
Contributor

Choose a reason for hiding this comment

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

wait what we read props.defaultChecked and assign it to node.checked?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the case when 'checked' is deleted. It's the same as setting it to null below.

Copy link
Contributor

Choose a reason for hiding this comment

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

ahh got it

@@ -1364,28 +1449,59 @@ export function updateProperties(
didWarnControlledToUncontrolled = true;
}
}

// Update checked *before* name.
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this isn't the same as the behavior before. If you update from

<input type="checkbox" name="a" checked={true} />

to

<input type="radio" name="b" checked={false} />

then I believe now it will uncheck other a radios as well as other b radios (!) but the old code didn't. I think if only name and checked change then you didn't regress, but also I'm unsure how much it matters given the other bug I noticed where this handling doesn't really reliably work for name and checked anyway…?

The best strategy I could come up with that seemed obviously correct in all cases was to unset name before changing either type/checked and then set it back after. (You could also set checked to false but I was having trouble thinking about how that affects the dirty checkedness flag and interplay with the attribute.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Before it would always set checked before the loop. It would sometimes set it again inside the loop, before or after "name" depending on enumeration order of the props.

Now it will consistently always set checked first and name later.

Are you saying I should set type after the loop too?

I guess we just need to fix it properly.

Copy link
Contributor

@sophiebits sophiebits Apr 11, 2023

Choose a reason for hiding this comment

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

Basically it seems you want to avoid any intermediate states that have simultaneously node.type === 'radio' and node.checked === true and node.name nonempty. Which is tricky because you could be transitioning either from that or to that. I don't think any of the six possible (type, name, checked) permutations is always correct. So either you need to be really fancy about what values are changing and set them in different orders as a result, or you need to unset one of the properties, update the other two, then set the first.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We already do that here:

https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/client/ReactDOMInput.js#L239

I think maybe this one is just here because of the weird case where checked is also set in the props - unlike value. It seems like maybe we should also use the special cases in updateInput and not update checked nor type in the loop.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, great. Yeah if we only set type and checked within that block then I think it's chill.

@sebmarkbage sebmarkbage merged commit 6b90976 into facebook:main Apr 11, 2023
github-actions bot pushed a commit that referenced this pull request Apr 11, 2023
…led components (#26596)

Since `props.x` is a possibly megamorphic access, it can be slow to
access and trigger recompilation.

When we are looping over the props and pattern matching every key,
anyway, we've already done this work. We can just reuse the same value
by stashing it outside the loop in the stack.

This only makes sense for updates in diffInCommitPhase since otherwise
we don't have the full set of props in that loop.

We also have to be careful not to skip over equal values since we need
to extract them anyway.

DiffTrain build for [6b90976](6b90976)
kassens added a commit to kassens/react that referenced this pull request Apr 17, 2023
sebmarkbage added a commit that referenced this pull request Apr 19, 2023
I accidentally made a behavior change in the refactor. It turns out that
when switching off `checked` to an uncontrolled component, we used to
revert to the concept of "initialChecked" which used to be stored on
state.

When there's a diff to this computed prop and the value of props.checked
is null, then we end up in a case where it sets `checked` to
`initialChecked`:


https://github.com/facebook/react/blob/5cbe6258bc436b1683080a6d978c27849f1d9a22/packages/react-dom-bindings/src/client/ReactDOMInput.js#L69

Since we never changed `initialChecked` and it's not relevant if
non-null `checked` changes value, the only way this "change" could
trigger was if we move from having `checked` to having null.

This wasn't really consistent with how `value` works, where we instead
leave the current value in place regardless. So this is a "bug fix" that
changes `checked` to be consistent with `value` and just leave the
current value in place. This case should already have a warning in it
regardless since it's going from controlled to uncontrolled.

Related to that, there was also another issue observed in
#26596 (comment) and
#26588

We need to atomically apply mutations on radio buttons. I fixed this by
setting the name to empty before doing mutations to value/checked/type
in updateInput, and then set the name to whatever it should be. Setting
the name is what ends up atomically applying the changes.

---------

Co-authored-by: Sophie Alpert <git@sophiebits.com>
github-actions bot pushed a commit that referenced this pull request Apr 19, 2023
I accidentally made a behavior change in the refactor. It turns out that
when switching off `checked` to an uncontrolled component, we used to
revert to the concept of "initialChecked" which used to be stored on
state.

When there's a diff to this computed prop and the value of props.checked
is null, then we end up in a case where it sets `checked` to
`initialChecked`:

https://github.com/facebook/react/blob/5cbe6258bc436b1683080a6d978c27849f1d9a22/packages/react-dom-bindings/src/client/ReactDOMInput.js#L69

Since we never changed `initialChecked` and it's not relevant if
non-null `checked` changes value, the only way this "change" could
trigger was if we move from having `checked` to having null.

This wasn't really consistent with how `value` works, where we instead
leave the current value in place regardless. So this is a "bug fix" that
changes `checked` to be consistent with `value` and just leave the
current value in place. This case should already have a warning in it
regardless since it's going from controlled to uncontrolled.

Related to that, there was also another issue observed in
#26596 (comment) and
#26588

We need to atomically apply mutations on radio buttons. I fixed this by
setting the name to empty before doing mutations to value/checked/type
in updateInput, and then set the name to whatever it should be. Setting
the name is what ends up atomically applying the changes.

---------

Co-authored-by: Sophie Alpert <git@sophiebits.com>

DiffTrain build for [1f248bd](1f248bd)
kassens pushed a commit to kassens/react that referenced this pull request Apr 21, 2023
, facebook#26595, facebook#26596, facebook#26627

- Refactor some controlled component stuff (facebook#26573)
- Don't update textarea defaultValue and input checked unnecessarily (facebook#26580)
- Diff properties in the commit phase instead of generating an update payload (facebook#26583)
- Move validation of text nesting into ReactDOMComponent  (facebook#26594)
- Remove initOption special case (facebook#26595)
- Use already extracted values instead of reading off props for controlled components (facebook#26596)
- Fix input tracking bug (facebook#26627)
github-actions bot pushed a commit that referenced this pull request Apr 21, 2023
- Refactor some controlled component stuff (#26573)
- Don't update textarea defaultValue and input checked unnecessarily (#26580)
- Diff properties in the commit phase instead of generating an update payload (#26583)
- Move validation of text nesting into ReactDOMComponent  (#26594)
- Remove initOption special case (#26595)
- Use already extracted values instead of reading off props for controlled components (#26596)
- Fix input tracking bug (#26627)

DiffTrain build for [ded4a78](ded4a78)
kassens pushed a commit that referenced this pull request Apr 21, 2023
- Refactor some controlled component stuff (#26573)
- Don't update textarea defaultValue and input checked unnecessarily (#26580)
- Diff properties in the commit phase instead of generating an update payload (#26583)
- Move validation of text nesting into ReactDOMComponent  (#26594)
- Remove initOption special case (#26595)
- Use already extracted values instead of reading off props for controlled components (#26596)
- Fix input tracking bug (#26627)
kassens pushed a commit that referenced this pull request Apr 21, 2023
I accidentally made a behavior change in the refactor. It turns out that
when switching off `checked` to an uncontrolled component, we used to
revert to the concept of "initialChecked" which used to be stored on
state.

When there's a diff to this computed prop and the value of props.checked
is null, then we end up in a case where it sets `checked` to
`initialChecked`:


https://github.com/facebook/react/blob/5cbe6258bc436b1683080a6d978c27849f1d9a22/packages/react-dom-bindings/src/client/ReactDOMInput.js#L69

Since we never changed `initialChecked` and it's not relevant if
non-null `checked` changes value, the only way this "change" could
trigger was if we move from having `checked` to having null.

This wasn't really consistent with how `value` works, where we instead
leave the current value in place regardless. So this is a "bug fix" that
changes `checked` to be consistent with `value` and just leave the
current value in place. This case should already have a warning in it
regardless since it's going from controlled to uncontrolled.

Related to that, there was also another issue observed in
#26596 (comment) and
#26588

We need to atomically apply mutations on radio buttons. I fixed this by
setting the name to empty before doing mutations to value/checked/type
in updateInput, and then set the name to whatever it should be. Setting
the name is what ends up atomically applying the changes.

---------

Co-authored-by: Sophie Alpert <git@sophiebits.com>
jerrydev0927 added a commit to jerrydev0927/react that referenced this pull request Jan 5, 2024
I accidentally made a behavior change in the refactor. It turns out that
when switching off `checked` to an uncontrolled component, we used to
revert to the concept of "initialChecked" which used to be stored on
state.

When there's a diff to this computed prop and the value of props.checked
is null, then we end up in a case where it sets `checked` to
`initialChecked`:

https://github.com/facebook/react/blob/5cbe6258bc436b1683080a6d978c27849f1d9a22/packages/react-dom-bindings/src/client/ReactDOMInput.js#L69

Since we never changed `initialChecked` and it's not relevant if
non-null `checked` changes value, the only way this "change" could
trigger was if we move from having `checked` to having null.

This wasn't really consistent with how `value` works, where we instead
leave the current value in place regardless. So this is a "bug fix" that
changes `checked` to be consistent with `value` and just leave the
current value in place. This case should already have a warning in it
regardless since it's going from controlled to uncontrolled.

Related to that, there was also another issue observed in
facebook/react#26596 (comment) and
facebook/react#26588

We need to atomically apply mutations on radio buttons. I fixed this by
setting the name to empty before doing mutations to value/checked/type
in updateInput, and then set the name to whatever it should be. Setting
the name is what ends up atomically applying the changes.

---------

Co-authored-by: Sophie Alpert <git@sophiebits.com>

DiffTrain build for [1f248bdd7199979b050e4040ceecfe72dd977fd1](facebook/react@1f248bd)
EdisonVan pushed a commit to EdisonVan/react that referenced this pull request Apr 15, 2024
…led components (facebook#26596)

Since `props.x` is a possibly megamorphic access, it can be slow to
access and trigger recompilation.

When we are looping over the props and pattern matching every key,
anyway, we've already done this work. We can just reuse the same value
by stashing it outside the loop in the stack.

This only makes sense for updates in diffInCommitPhase since otherwise
we don't have the full set of props in that loop.

We also have to be careful not to skip over equal values since we need
to extract them anyway.
EdisonVan pushed a commit to EdisonVan/react that referenced this pull request Apr 15, 2024
)

I accidentally made a behavior change in the refactor. It turns out that
when switching off `checked` to an uncontrolled component, we used to
revert to the concept of "initialChecked" which used to be stored on
state.

When there's a diff to this computed prop and the value of props.checked
is null, then we end up in a case where it sets `checked` to
`initialChecked`:


https://github.com/facebook/react/blob/5cbe6258bc436b1683080a6d978c27849f1d9a22/packages/react-dom-bindings/src/client/ReactDOMInput.js#L69

Since we never changed `initialChecked` and it's not relevant if
non-null `checked` changes value, the only way this "change" could
trigger was if we move from having `checked` to having null.

This wasn't really consistent with how `value` works, where we instead
leave the current value in place regardless. So this is a "bug fix" that
changes `checked` to be consistent with `value` and just leave the
current value in place. This case should already have a warning in it
regardless since it's going from controlled to uncontrolled.

Related to that, there was also another issue observed in
facebook#26596 (comment) and
facebook#26588

We need to atomically apply mutations on radio buttons. I fixed this by
setting the name to empty before doing mutations to value/checked/type
in updateInput, and then set the name to whatever it should be. Setting
the name is what ends up atomically applying the changes.

---------

Co-authored-by: Sophie Alpert <git@sophiebits.com>
bigfootjon pushed a commit that referenced this pull request Apr 18, 2024
…led components (#26596)

Since `props.x` is a possibly megamorphic access, it can be slow to
access and trigger recompilation.

When we are looping over the props and pattern matching every key,
anyway, we've already done this work. We can just reuse the same value
by stashing it outside the loop in the stack.

This only makes sense for updates in diffInCommitPhase since otherwise
we don't have the full set of props in that loop.

We also have to be careful not to skip over equal values since we need
to extract them anyway.

DiffTrain build for commit 6b90976.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants