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

Update tracked value after resetting radio group #27394

Merged
merged 3 commits into from
Sep 21, 2023
Merged

Update tracked value after resetting radio group #27394

merged 3 commits into from
Sep 21, 2023

Conversation

sophiebits
Copy link
Collaborator

Fixes #26876, I think. Review each commit separately (all assertions pass in main already, except the last assertInputTrackingIsClean in "should control radio buttons").

I'm actually a little confused on two things here:

  • All the isCheckedDirty assertions are true. But I don't think we set .checked unconditionally? So how does this happen?
  • Bug: Radio button onChange not called in current React Canary #26876 (comment) claims that d962f35...1f248bd contains the faulty change, but it doesn't appear to change the restoration logic that I've touched here. (One difference outside restoration is that updateProperties did previously set .checked when nextProp !== lastProp whereas the new logic in updateInput is to set it when node.checked !== !!checked.)

But it seems to me like we need this call here anyway, and if it fixes it then it fixes it? I think technically speaking we probably should do all the updateInput() calls and then all the updateValueIfChanged() calls—in particular I think if clicking A changed the checked radio button from B to C then the code as I have it would be incorrect, but that also seems unlikely so idk whether to care.

cc @zhengjitf @Luk-z who did some investigation on the original issue

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

react-sizebot commented Sep 20, 2023

Comparing: 2807d78...26daa54

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.03% 166.62 kB 166.66 kB +0.02% 52.13 kB 52.14 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.03% 176.05 kB 176.10 kB +0.01% 54.97 kB 54.97 kB
facebook-www/ReactDOM-prod.classic.js +0.04% 571.73 kB 571.96 kB +0.02% 100.64 kB 100.66 kB
facebook-www/ReactDOM-prod.modern.js +0.04% 555.46 kB 555.68 kB +0.02% 97.75 kB 97.77 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 26daa54

@eps1lon
Copy link
Collaborator

eps1lon commented Sep 20, 2023

This also fixes #19860 😮 : https://codesandbox.io/s/react-18-radio-cannot-be-read-only-forked-vrh6v2?file=/src/App.js

I don't have much context about this type of code but the fact that it fixes the original issue and another one, is a pretty strong signal for me.

@sebmarkbage
Copy link
Collaborator

All the isCheckedDirty assertions are true. But I don't think we set .checked unconditionally? So how does this happen?

This is unconditionally set:

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

I don't think we do for value which is a suspicious asymmetry that I've noted before.

@sophiebits
Copy link
Collaborator Author

Hmm it does seem like there is probably a hydration bug too based on reports at vercel/next.js#49499.

@sebmarkbage
Copy link
Collaborator

The isHydrating check is suspicious. I don't think that's quite right. If we do need to set it e.g. to set the dirty flag or update the input value tracking, then we'd have to do that for hydration too. The reason we don't want to do it for hydration is so that it doesn't reset. But maybe what we need to do is just element.value = element.value or element.checked = element.checked.

@sophiebits
Copy link
Collaborator Author

I think I'll update this to call updateValueIfChanged() on all the inputs after calling updateInput() on all of them. Will also rename "assertInputTrackingIsClean" to not use the word "Clean" since that could seem related to "Dirty" but it's not.

In a separate PR I'll plan to add a test for hydration. My expectation is if you interact with radios before the page hydrates then onChange will not fire for that but it will work normally following that (meaning the dirty flag needs to be set and value tracking needs to be up to date).

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Sep 20, 2023

I kind of think onChange should fire after the fact on inputs after hydration if it changed. At least for controlled so that it can enter the correct state. We had the same issue that onLoad for img should probably fire if it loaded before hydration. Maybe for 19? We currently don't really have a commit effect for hydration on DOM nodes which limits stuff like this.

sophiebits added a commit that referenced this pull request Sep 21, 2023
Fixes whatever part of #26876 and vercel/next.js#49499 that #27394 didn't fix, probably.

From manual tests I believe this behavior brings us back to parity with latest stable release (18.2.0). It's awkward that we keep the user's state even for controlled inputs.

Previously the .defaultChecked assignment done in updateInput() was changing the actual checkedness because the dirty flag wasn't getting set, meaning that hydrating could change which radio button is checked, even in the absence of user interaction! Now we go back to always detaching again.
sophiebits added a commit that referenced this pull request Sep 21, 2023
Fixes whatever part of #26876 and vercel/next.js#49499 that #27394 didn't fix, probably.

From manual tests I believe this behavior brings us back to parity with latest stable release (18.2.0). It's awkward that we keep the user's state even for controlled inputs.

Previously the .defaultChecked assignment done in updateInput() was changing the actual checkedness because the dirty flag wasn't getting set, meaning that hydrating could change which radio button is checked, even in the absence of user interaction! Now we go back to always detaching again.
@sophiebits sophiebits merged commit 3c27178 into main Sep 21, 2023
2 checks passed
@sophiebits sophiebits deleted the gh-26876 branch September 21, 2023 04:58
sophiebits added a commit that referenced this pull request Sep 21, 2023
Fixes whatever part of #26876 and vercel/next.js#49499 that #27394 didn't fix, probably.

From manual tests I believe this behavior brings us back to parity with latest stable release (18.2.0). It's awkward that we keep the user's state even for controlled inputs.

Previously the .defaultChecked assignment done in updateInput() was changing the actual checkedness because the dirty flag wasn't getting set, meaning that hydrating could change which radio button is checked, even in the absence of user interaction! Now we go back to always detaching again.
github-actions bot pushed a commit that referenced this pull request Sep 21, 2023
Fixes #26876, I think. Review each commit separately (all assertions
pass in main already, except the last assertInputTrackingIsClean in
"should control radio buttons").

I'm actually a little confused on two things here:
* All the isCheckedDirty assertions are true. But I don't think we set
.checked unconditionally? So how does this happen?
* #26876 (comment)
claims that
d962f35...1f248bd contains
the faulty change, but it doesn't appear to change the restoration logic
that I've touched here. (One difference outside restoration is that
updateProperties did previously set `.checked` when `nextProp !==
lastProp` whereas the new logic in updateInput is to set it when
`node.checked !== !!checked`.)

But it seems to me like we need this call here anyway, and if it fixes
it then it fixes it? I think technically speaking we probably should do
all the updateInput() calls and then all the updateValueIfChanged()
calls—in particular I think if clicking A changed the checked radio
button from B to C then the code as I have it would be incorrect, but
that also seems unlikely so idk whether to care.

cc @zhengjitf @Luk-z who did some investigation on the original issue

DiffTrain build for [3c27178](3c27178)
@Luk-z
Copy link

Luk-z commented Sep 30, 2023

Sorry for late response.
Tried with 18.3.0-canary-d900fadbf-20230929 the problem still persists.
Working example with 18.3.0-next-d962f35ca-20230418
Not working example with 18.3.0-canary-d900fadbf-20230929

@sophiebits didn't know if this PR is anymore the right place to discuss about (also saw that #26876 was closed )

@sophiebits
Copy link
Collaborator Author

@Luk-z Thank you for your diligence. I have now posted a fix in #27443 and verified that it fixes this repro case:

https://codesandbox.io/s/react-canary-radio-buttons-latest-not-working-forked-t2wwyc

@Luk-z
Copy link

Luk-z commented Oct 1, 2023

@sophiebits I confirm that it works now, thanks!

sophiebits added a commit that referenced this pull request Oct 2, 2023
Fixes whatever part of #26876
and vercel/next.js#49499 that
#27394 didn't fix, probably.

From manual tests I believe this behavior brings us back to parity with
latest stable release (18.2.0). It's awkward that we keep the user's
state even for controlled inputs, so the DOM is out of sync with React
state.

Previously the .defaultChecked assignment done in updateInput() was
changing the actual checkedness because the dirty flag wasn't getting
set, meaning that hydrating could change which radio button is checked,
even in the absence of user interaction! Now we go back to always
detaching again.
github-actions bot pushed a commit that referenced this pull request Oct 3, 2023
Fixes whatever part of #26876
and vercel/next.js#49499 that
#27394 didn't fix, probably.

From manual tests I believe this behavior brings us back to parity with
latest stable release (18.2.0). It's awkward that we keep the user's
state even for controlled inputs, so the DOM is out of sync with React
state.

Previously the .defaultChecked assignment done in updateInput() was
changing the actual checkedness because the dirty flag wasn't getting
set, meaning that hydrating could change which radio button is checked,
even in the absence of user interaction! Now we go back to always
detaching again.

DiffTrain build for [db69f95](db69f95)
alunyov pushed a commit to alunyov/react that referenced this pull request Oct 11, 2023
Fixes whatever part of facebook#26876
and vercel/next.js#49499 that
facebook#27394 didn't fix, probably.

From manual tests I believe this behavior brings us back to parity with
latest stable release (18.2.0). It's awkward that we keep the user's
state even for controlled inputs, so the DOM is out of sync with React
state.

Previously the .defaultChecked assignment done in updateInput() was
changing the actual checkedness because the dirty flag wasn't getting
set, meaning that hydrating could change which radio button is checked,
even in the absence of user interaction! Now we go back to always
detaching again.
jerrydev0927 added a commit to jerrydev0927/react that referenced this pull request Jan 5, 2024
Fixes whatever part of facebook/react#26876
and vercel/next.js#49499 that
facebook/react#27394 didn't fix, probably.

From manual tests I believe this behavior brings us back to parity with
latest stable release (18.2.0). It's awkward that we keep the user's
state even for controlled inputs, so the DOM is out of sync with React
state.

Previously the .defaultChecked assignment done in updateInput() was
changing the actual checkedness because the dirty flag wasn't getting
set, meaning that hydrating could change which radio button is checked,
even in the absence of user interaction! Now we go back to always
detaching again.

DiffTrain build for [db69f95e4876ec3c24117f58d55cbb4f315b9fa7](facebook/react@db69f95)
jerrydev0927 added a commit to jerrydev0927/react that referenced this pull request Jan 5, 2024
Fixes whatever part of facebook/react#26876 and vercel/next.js#49499 that facebook/react#27394 didn't fix, probably.

From manual tests I believe this behavior brings us back to parity with latest stable release (18.2.0). It's awkward that we keep the user's state even for controlled inputs.

Previously the .defaultChecked assignment done in updateInput() was changing the actual checkedness because the dirty flag wasn't getting set, meaning that hydrating could change which radio button is checked, even in the absence of user interaction! Now we go back to always detaching again.
EdisonVan pushed a commit to EdisonVan/react that referenced this pull request Apr 15, 2024
Fixes facebook#26876, I think. Review each commit separately (all assertions
pass in main already, except the last assertInputTrackingIsClean in
"should control radio buttons").

I'm actually a little confused on two things here:
* All the isCheckedDirty assertions are true. But I don't think we set
.checked unconditionally? So how does this happen?
* facebook#26876 (comment)
claims that
facebook/react@d962f35...1f248bd contains
the faulty change, but it doesn't appear to change the restoration logic
that I've touched here. (One difference outside restoration is that
updateProperties did previously set `.checked` when `nextProp !==
lastProp` whereas the new logic in updateInput is to set it when
`node.checked !== !!checked`.)

But it seems to me like we need this call here anyway, and if it fixes
it then it fixes it? I think technically speaking we probably should do
all the updateInput() calls and then all the updateValueIfChanged()
calls—in particular I think if clicking A changed the checked radio
button from B to C then the code as I have it would be incorrect, but
that also seems unlikely so idk whether to care.

cc @zhengjitf @Luk-z who did some investigation on the original issue
EdisonVan pushed a commit to EdisonVan/react that referenced this pull request Apr 15, 2024
Fixes whatever part of facebook#26876
and vercel/next.js#49499 that
facebook#27394 didn't fix, probably.

From manual tests I believe this behavior brings us back to parity with
latest stable release (18.2.0). It's awkward that we keep the user's
state even for controlled inputs, so the DOM is out of sync with React
state.

Previously the .defaultChecked assignment done in updateInput() was
changing the actual checkedness because the dirty flag wasn't getting
set, meaning that hydrating could change which radio button is checked,
even in the absence of user interaction! Now we go back to always
detaching again.
bigfootjon pushed a commit that referenced this pull request Apr 18, 2024
Fixes #26876, I think. Review each commit separately (all assertions
pass in main already, except the last assertInputTrackingIsClean in
"should control radio buttons").

I'm actually a little confused on two things here:
* All the isCheckedDirty assertions are true. But I don't think we set
.checked unconditionally? So how does this happen?
* #26876 (comment)
claims that
d962f35...1f248bd contains
the faulty change, but it doesn't appear to change the restoration logic
that I've touched here. (One difference outside restoration is that
updateProperties did previously set `.checked` when `nextProp !==
lastProp` whereas the new logic in updateInput is to set it when
`node.checked !== !!checked`.)

But it seems to me like we need this call here anyway, and if it fixes
it then it fixes it? I think technically speaking we probably should do
all the updateInput() calls and then all the updateValueIfChanged()
calls—in particular I think if clicking A changed the checked radio
button from B to C then the code as I have it would be incorrect, but
that also seems unlikely so idk whether to care.

cc @zhengjitf @Luk-z who did some investigation on the original issue

DiffTrain build for commit 3c27178.
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.

Bug: Radio button onChange not called in current React Canary
6 participants