Change:
<!-- Please provide a clear and concise description of what the bug is. Include screenshots if needed. Please test using the latest version of the relevant React packages to make sure your issue has not already been fixed. --React version: 18.3.0-canary-- radio buttons that toggle in selecting each radio button, is no longer called <!-- Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Issues without reproduction steps or code examples may be immediately closed as not actionable. --Link 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 <!-- Please provide a CodeSandbox (), a link to a repository on GitHub, or provide a minimal code example that reproduces the problem. You may provide a screenshot of the application if you think it is relevant to your bug report. Here are some tips for providing a minimal example: --'s prop is not called on subsequent clicks of the input 's prop should be called on subsequent clicks of the input
As far as I an tell this also repros without toggling : So it's just about changing the controlled delayed.
The problem was introduced with . The previous version works.
It looks like it was this change that introduced this bug:
what are ways to fix this issue?
FYI, I find something by investigating, as below: : isn't called because here is falsy in that process. PS: In our case, should be equal to . Diving into the inner functions as the followings. : : : We can find the key is that the tracked value should be different from the value get from () in our case. We can believe the tracked value was not updated after the delayed state change, which cause the issue. Analysis by reproduction steps (Please visit , which can print the tracked values): : One: checked ( is ), and the tracked value is Two: unchecked, and the tracked value is 'Two', and wait for delayed : is called One: unchecked, and the tracked value is Two: checked, and the tracked value is 'One', and wait for the delayed : is called One: checked, and the tracked value is Two: unchecked, and the tracked value is 'Two', won't call again PS: The tracked value can be updated at the end of the event handler (by ), so Step 2 will result in the correct tracked values. 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 between a working commit () and non-working commit () and figured out that the bug happens after removing in function of . just restoring or just adding the bug disappear. Maybe the problem is explained in this is not updated and will result null/undefined and the radio is no more controlled? You can check this . I've also made a change to to start devtools server with a simple radio example. To run the server: run in the root directory and in directory. from to the root directory run from folder execute (better to open another shell for this command) go to

Discussion:
} } <del> if (checked != null && node.checked !== !!checked) { node.checked = checked; </del> <ins> if (checked != null) { // Important to set this even if it's not a change in order to update input // value tracking with radio buttons // TODO: Should really update input value tracking for the whole radio // button group in an effect or something (similar to #27024) node.checked = checked && typeof checked !== 'function' && typeof checked !== 'symbol'; </ins> } if (

it('should not incur unnecessary DOM mutations for boolean properties', () => { const container = document.createElement('div'); <del> function onChange() { // noop } ReactDOM.render( <input type="checkbox" onChange={onChange} checked={true} />, container, ); </del> <ins> ReactDOM.render(<audio muted={true} />, container); </ins> const node = container.firstChild; let nodeValue = true; const nodeValueSetter = jest.fn(); <del> Object.defineProperty(node, 'checked', { </del> <ins> Object.defineProperty(node, 'muted', { </ins> get: function () { return nodeValue; },

}), }); <del> ReactDOM.render( <input type="checkbox" onChange={onChange} checked={true} data-unrelated={true} />, container, ); expect(nodeValueSetter).toHaveBeenCalledTimes(0); expect(() => { ReactDOM.render( <input type="checkbox" onChange={onChange} />, container, ); }).toErrorDev( 'A component is changing a controlled input to be uncontrolled. This is likely caused by ' + 'the value changing from a defined to undefined, which should not happen. Decide between ' + 'using a controlled or uncontrolled input element for the lifetime of the component.', ); // This leaves the current checked value in place, just like text inputs. </del> <ins> ReactDOM.render(<audio muted={true} data-unrelated="yes" />, container); </ins> expect(nodeValueSetter).toHaveBeenCalledTimes(0); <del> expect(() => { ReactDOM.render( <input type="checkbox" onChange={onChange} checked={false} />, container, ); }).toErrorDev( ' A component is changing an uncontrolled input to be controlled. This is likely caused by ' + 'the value changing from undefined to a defined value, which should not happen. Decide between ' + 'using a controlled or uncontrolled input element for the lifetime of the component.', ); </del> <ins> ReactDOM.render(<audio muted={false} data-unrelated="ok" />, container); </ins> expect(nodeValueSetter).toHaveBeenCalledTimes(1); <del> ReactDOM.render( <input type="checkbox" onChange={onChange} checked={true} />, container, ); expect(nodeValueSetter).toHaveBeenCalledTimes(2); </del> }); it('should ignore attribute list for elements with the "is" attribute', () => {

assertInputTrackingIsCurrent(container); }); <ins> it('should control radio buttons if the tree updates during render (case 2; #26876)', () => { let thunk = null; function App() { const [disabled, setDisabled] = React.useState(false); const [value, setValue] = React.useState('one'); function handleChange(e) { setDisabled(true); // Pretend this is in a setTimeout or something thunk = () => { setDisabled(false); setValue(e.target.value); }; } return ( <> <input type="radio" name="fruit" value="one" checked={value === 'one'} onChange={handleChange} disabled={disabled} /> <input type="radio" name="fruit" value="two" checked={value === 'two'} onChange={handleChange} disabled={disabled} /> </> ); } ReactDOM.render(<App />, container); const [one, two] = container.querySelectorAll('input'); expect(one.checked).toBe(true); expect(two.checked).toBe(false); expect(isCheckedDirty(one)).toBe(true); expect(isCheckedDirty(two)).toBe(true); assertInputTrackingIsCurrent(container); // Click two setUntrackedChecked.call(two, true); dispatchEventOnNode(two, 'click'); expect(one.checked).toBe(true); expect(two.checked).toBe(false); expect(isCheckedDirty(one)).toBe(true); expect(isCheckedDirty(two)).toBe(true); assertInputTrackingIsCurrent(container); // After a delay... ReactDOM.unstable_batchedUpdates(thunk); expect(one.checked).toBe(false); expect(two.checked).toBe(true); expect(isCheckedDirty(one)).toBe(true); expect(isCheckedDirty(two)).toBe(true); assertInputTrackingIsCurrent(container); // Click back to one setUntrackedChecked.call(one, true); dispatchEventOnNode(one, 'click'); expect(one.checked).toBe(false); expect(two.checked).toBe(true); expect(isCheckedDirty(one)).toBe(true); expect(isCheckedDirty(two)).toBe(true); assertInputTrackingIsCurrent(container); // After a delay... ReactDOM.unstable_batchedUpdates(thunk); expect(one.checked).toBe(true); expect(two.checked).toBe(false); expect(isCheckedDirty(one)).toBe(true); expect(isCheckedDirty(two)).toBe(true); assertInputTrackingIsCurrent(container); }); </ins> it('should warn with value and no onChange handler and readOnly specified', () => { ReactDOM.render( <input type="text" value="zoink" readOnly={true} />,