Commit 987bfa3
authored
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-bce7d0be8828
Fixes: #37601 parent 030773c commit 987bfa3
File tree
2 files changed
+7
-1
lines changed- packages/@headlessui-react
- src/hooks
2 files changed
+7
-1
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
| 2 | + | |
2 | 3 | | |
3 | 4 | | |
4 | 5 | | |
| |||
33 | 34 | | |
34 | 35 | | |
35 | 36 | | |
36 | | - | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
37 | 42 | | |
38 | 43 | | |
39 | 44 | | |
| |||
0 commit comments