Skip to content

Switching checked to null should leave the current value #26667

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

Merged
merged 5 commits into from
Apr 19, 2023

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented 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:

checked: checked != null ? checked : node._wrapperState.initialChecked,

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.

sebmarkbage and others added 4 commits April 18, 2023 23:29
We set a blank name to disconnect it from a set of radio buttons, then
make the mutations, then set the name at the end to apply the new state to
the set.
```
 FAIL  packages/react-dom/src/__tests__/ReactDOMInput-test.js (8.01 s)
  ● ReactDOMInput › shouldn't get tricked by changing radio names, part 2

    expect(received).toBe(expected) // Object.is equality

    Expected: true
    Received: false

      1209 |       container
      1210 |     );
    > 1211 |     expect(container.querySelector('input[name="a"][value="1"]').checked).toBe(true);
           |                                                                               ^
      1212 |     expect(container.querySelector('input[name="b"][value="2"]').checked).toBe(true);
      1213 |   });
      1214 |

      at Object.<anonymous> (packages/react-dom/src/__tests__/ReactDOMInput-test.js:1211:79)
```
@sebmarkbage sebmarkbage requested a review from sophiebits April 19, 2023 04:46
@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Apr 19, 2023
@react-sizebot
Copy link

react-sizebot commented Apr 19, 2023

Comparing: d8089f2...12db62b

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 = 164.49 kB 164.41 kB = 51.69 kB 51.68 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 166.96 kB 166.77 kB = 52.37 kB 52.32 kB
facebook-www/ReactDOM-prod.classic.js = 565.31 kB 563.69 kB = 99.48 kB 99.34 kB
facebook-www/ReactDOM-prod.modern.js = 549.10 kB 547.43 kB = 96.79 kB 96.62 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
facebook-www/ReactDOM-profiling.classic.js = 595.82 kB 594.20 kB = 104.01 kB 103.87 kB
facebook-www/ReactDOMTesting-prod.classic.js = 579.79 kB 578.17 kB = 103.16 kB 103.02 kB
facebook-www/ReactDOM-prod.classic.js = 565.31 kB 563.69 kB = 99.48 kB 99.34 kB
facebook-www/ReactDOM-profiling.modern.js = 579.54 kB 577.86 kB = 101.28 kB 101.12 kB
facebook-www/ReactDOMTesting-prod.modern.js = 565.64 kB 563.97 kB = 100.96 kB 100.81 kB
facebook-www/ReactDOM-prod.modern.js = 549.10 kB 547.43 kB = 96.79 kB 96.62 kB

Generated by 🚫 dangerJS against 12db62b

if (__DEV__) {
checkAttributeStringCoercion(type, 'type');
}
node.type = type;
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 always setting the type instead of only if it has changed. Not sure if that would matter like resetting dirty flags.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it's fine? from the spec it sounds like it would only be the value sanitization algorithm but I tested https://codesandbox.io/s/crazy-ace-rc85uh?file=/src/App.js in chrome, safari, and firefox and none of them seem to do anything when reassigning type to the same value (you can type non-numbers like the letter e and they don't go away when the timer fires).

// Changing the type or name as the same time as changing the checked value
// needs to be atomically applied. We can only ensure that by disconnecting
// the name while do the mutations and then reapply the name after that's done.
node.name = '';
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Similarly, we didn't used to do this in the update phase - only init phase. Not sure if it'll mess with some internal states.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it should be good

if (__DEV__) {
checkAttributeStringCoercion(type, 'type');
}
node.type = type;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it's fine? from the spec it sounds like it would only be the value sanitization algorithm but I tested https://codesandbox.io/s/crazy-ace-rc85uh?file=/src/App.js in chrome, safari, and firefox and none of them seem to do anything when reassigning type to the same value (you can type non-numbers like the letter e and they don't go away when the timer fires).

// Changing the type or name as the same time as changing the checked value
// needs to be atomically applied. We can only ensure that by disconnecting
// the name while do the mutations and then reapply the name after that's done.
node.name = '';
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it should be good

}
node.name = name;
} else {
node.removeAttribute('name');
Copy link
Collaborator

Choose a reason for hiding this comment

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

is it better to removeAttribute above instead of = '' so you don't need to do it here?

node.name = name;
} else {
node.removeAttribute('name');
Copy link
Collaborator

Choose a reason for hiding this comment

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

is it already unset? or for hydration it's not? in which case do you still need the previous clearing that was in the old code?

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 missed this one. I'll remove. It's a tricky one because it's a hydration error in DEV regardless. We don't try to patch up other things so is there a reason we'd consider this case important? Seems like not.

In fact, I have another todo to reconsider if we should even call this method at all during hydration. Seems like we should dispatch a change event if it's different and then trigger the restore mechanism but that would have to happen after commit.

We won't clean up hydration.
@sebmarkbage sebmarkbage merged commit 1f248bd into facebook:main Apr 19, 2023
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 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>
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
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 commit 1f248bd.
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