You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Ensure form state is up to date when using uncontrolled components (#3790)
This PR fixes an issue where uncontrolled `Switch` or `Checkbox`
components' form state aren't always up to date when calling the
`onChange` handler.
This is because the `onChange` handler is called at the same time the
internal state is updated. That means that if you submit the nearest
form as part of the `onChange` handler that the form state is not up to
date yet.
We fix this by calling `flushSync()` before we call the `onChange`
handler when dealing with an uncontrolled component.
## Test plan
Setup a small reproduction with both a controlled and uncontrolled
checkbox
<details>
<summary>Reproduction</summary>
```tsx
import { Switch } from '@headlessui/react'
import { useRef, useState } from 'react'
import { flushSync } from 'react-dom'
export default function App() {
let formRef = useRef<HTMLFormElement>(null)
let [enabled, setEnabled] = useState(true)
let [submisisons, setSubmissions] = useState<Array<Record<string, string>>>([])
return (
<div>
<form
className="p-8"
ref={formRef}
onSubmit={(e) => {
e.preventDefault()
let form = Object.fromEntries(new FormData(e.currentTarget).entries()) as Record<
string,
string
>
setSubmissions((s) => s.concat([form]))
}}
>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<Switch
name="my-uncontrolled-switch"
className="data-checked:bg-blue-500 aspect-square rounded border bg-white p-2"
defaultChecked={true}
onChange={() => {
formRef.current?.requestSubmit()
}}
/>
<div>Uncontrolled switch</div>
</div>
<div className="flex items-center gap-2">
<Switch
name="my-controlled-switch"
className="data-checked:bg-blue-500 aspect-square rounded border bg-white p-2"
checked={enabled}
defaultChecked={true}
onChange={(v) => {
// If you are controlling state yourself, then the `form` fields are
// based on the incoming value, so in this case you have to call the
// flushSync() yourself.
flushSync(() => setEnabled(v))
formRef.current?.requestSubmit()
}}
/>
<div>Controlled switch</div>
</div>
<button type="submit" className="mt-4 w-56 border">
Submit
</button>
</div>
<hr className="my-8" />
<h3>Form submisisons:</h3>
<pre>{JSON.stringify(submisisons.toReversed(), null, 2)}</pre>
</form>
</div>
)
}
```
</details>
Before:
Notice that the moment I click the `uncontrolled` checkbox, the form is
not up to date yet. Pressing submit again shows the correct value even
though visually nothing changed anymore.
https://github.com/user-attachments/assets/600eb3c2-9c56-40a7-900d-5694f391eced
After:
Here the form state is always up to date when submitting as part of the
`onChange` handler.
https://github.com/user-attachments/assets/640c9ed6-69d2-4d5b-acfc-bce7d0be8828Fixes: #3760
0 commit comments