-
Notifications
You must be signed in to change notification settings - Fork 46.9k
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
Bug: Radio button onChange not called in current React Canary #26876
Comments
As far as I an tell this also repros without toggling So it's just about changing the controlled |
The problem was introduced with |
It looks like it was this change that introduced this bug: #26667 |
what are ways to fix this issue? |
FYI, I find something by investigating, as below:
react/packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js Lines 322 to 331 in 6aacd3f
Diving into the inner functions as the followings.
react/packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js Lines 117 to 122 in 6aacd3f
react/packages/react-dom-bindings/src/client/inputValueTracking.js Lines 141 to 147 in 6aacd3f
react/packages/react-dom-bindings/src/client/inputValueTracking.js Lines 103 to 111 in 6aacd3f
We can find the key is that the tracked value should be different from the value get from Analysis by reproduction steps (Please visit this demo, which can print the tracked values):
PS: The tracked value can be updated at the end of the event handler (by So, I think one solution is to update the tracked value (at the end of the commit mutation phase) when the checked prop changes. I'd like to submit a PR to fix it. Any corrections or suggestions will be appreciated, (for I'm not sure if I miss anything important). |
I tried another way to understand what caused the bug. I just checked the diff between a working commit (d962f35) and non-working commit (1f248bd) and figured out that the bug happens after removing this lines of code in just restoring
or just adding Maybe the problem is explained in this comment
You can check this branch. I've also made a change to
React.b.mov |
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>
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.
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.
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
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.
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)
Fixes #26876 for real? In 18.2.0 (last stable), we set .checked unconditionally: https://github.com/facebook/react/blob/v18.2.0/packages/react-dom/src/client/ReactDOMInput.js#L129-L135 This is important because if we are updating two radios' checkedness from (false, true) to (true, false), we need to make sure that input2.checked is explicitly set to false, even though setting `input1.checked = true` already unchecks input2. I think this fix is not complete because there is no guarantee that all the inputs rerender at the same time? Hence the TODO. But in practice they usually would and I _think_ this is comparable to what we had before. Also treating function and symbol as false like we used to and like we do on initial mount.
Fixes #26876 for real? In 18.2.0 (last stable), we set .checked unconditionally: https://github.com/facebook/react/blob/v18.2.0/packages/react-dom/src/client/ReactDOMInput.js#L129-L135 This is important because if we are updating two radios' checkedness from (false, true) to (true, false), we need to make sure that input2.checked is explicitly set to false, even though setting `input1.checked = true` already unchecks input2. I think this fix is not complete because there is no guarantee that all the inputs rerender at the same time? Hence the TODO. But in practice they usually would and I _think_ this is comparable to what we had before. Also treating function and symbol as false like we used to and like we do on initial mount.
Fixes #26876 for real? In 18.2.0 (last stable), we set .checked unconditionally: https://github.com/facebook/react/blob/v18.2.0/packages/react-dom/src/client/ReactDOMInput.js#L129-L135 This is important because if we are updating two radios' checkedness from (false, true) to (true, false), we need to make sure that input2.checked is explicitly set to false, even though setting `input1.checked = true` already unchecks input2. I think this fix is not complete because there is no guarantee that all the inputs rerender at the same time? Hence the TODO. But in practice they usually would and I _think_ this is comparable to what we had before. Also treating function and symbol as false like we used to and like we do on initial mount.
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.
Fixes #26876 for real? In 18.2.0 (last stable), we set .checked unconditionally: https://github.com/facebook/react/blob/v18.2.0/packages/react-dom/src/client/ReactDOMInput.js#L129-L135 This is important because if we are updating two radios' checkedness from (false, true) to (true, false), we need to make sure that input2.checked is explicitly set to false, even though setting `input1.checked = true` already unchecks input2. I think this fix is not complete because there is no guarantee that all the inputs rerender at the same time? Hence the TODO. But in practice they usually would and I _think_ this is comparable to what we had before. Also treating function and symbol as false like we used to and like we do on initial mount. DiffTrain build for [4f4c52a](4f4c52a)
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)
Fixes facebook#26876 for real? In 18.2.0 (last stable), we set .checked unconditionally: https://github.com/facebook/react/blob/v18.2.0/packages/react-dom/src/client/ReactDOMInput.js#L129-L135 This is important because if we are updating two radios' checkedness from (false, true) to (true, false), we need to make sure that input2.checked is explicitly set to false, even though setting `input1.checked = true` already unchecks input2. I think this fix is not complete because there is no guarantee that all the inputs rerender at the same time? Hence the TODO. But in practice they usually would and I _think_ this is comparable to what we had before. Also treating function and symbol as false like we used to and like we do on initial mount.
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.
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? * facebook/react#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 DiffTrain build for [3c27178a2f2c74f14d90613028e3929e1f06d830](facebook/react@3c27178)
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)
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.
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
Fixes facebook#26876 for real? In 18.2.0 (last stable), we set .checked unconditionally: https://github.com/facebook/react/blob/v18.2.0/packages/react-dom/src/client/ReactDOMInput.js#L129-L135 This is important because if we are updating two radios' checkedness from (false, true) to (true, false), we need to make sure that input2.checked is explicitly set to false, even though setting `input1.checked = true` already unchecks input2. I think this fix is not complete because there is no guarantee that all the inputs rerender at the same time? Hence the TODO. But in practice they usually would and I _think_ this is comparable to what we had before. Also treating function and symbol as false like we used to and like we do on initial mount.
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.
Fixes #26876 for real? In 18.2.0 (last stable), we set .checked unconditionally: https://github.com/facebook/react/blob/v18.2.0/packages/react-dom/src/client/ReactDOMInput.js#L129-L135 This is important because if we are updating two radios' checkedness from (false, true) to (true, false), we need to make sure that input2.checked is explicitly set to false, even though setting `input1.checked = true` already unchecks input2. I think this fix is not complete because there is no guarantee that all the inputs rerender at the same time? Hence the TODO. But in practice they usually would and I _think_ this is comparable to what we had before. Also treating function and symbol as false like we used to and like we do on initial mount. DiffTrain build for commit 4f4c52a.
React version: 18.3.0-canary-a1f97589f-20230526
Steps To Reproduce
disabled
inonChange
onChange
is no longer calledLink to code example:
The following CodeSandbox demonstrates the issue with the current react canary version. The issue is not present when react & react-dom versions are changed to stable 18.2.0
https://codesandbox.io/s/react-canary-radio-buttons-deiqb3?file=/src/App.js
The current behavior
<input type="radio" />
'sonChange
prop is not called on subsequent clicks of the inputThe expected behavior
<input type="radio" />
'sonChange
prop should be called on subsequent clicks of the inputThe text was updated successfully, but these errors were encountered: