Description
When using onSubmitAsync for form validation, the isSubmitting state is set to true correctly, but the browser doesn't have time to paint the updated UI (e.g., a loading spinner) before the async validator function starts executing.
Steps to Reproduce
- Create a form with
onSubmitAsync validator
- Subscribe to
isSubmitting to show a spinner
- In
onSubmitAsync, immediately call an async operation (e.g., API validation)
const form = useForm({
validators: {
onSubmitAsync: async ({ value }) => {
// Browser hasn't painted isSubmitting=true spinner yet!
const isValid = await validateApiKey(value.apiKey)
return isValid ? undefined : { form: 'Invalid API key' }
},
},
})
// In JSX:
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<Button disabled={isSubmitting}>
{isSubmitting ? <Spinner /> : 'Submit'}
</Button>
)}
</form.Subscribe>
Expected Behavior
The spinner should be visible immediately when the user clicks submit, before onSubmitAsync starts executing.
Actual Behavior
The spinner appears late (or not at all for fast operations) because:
handleSubmit() sets isSubmitting = true
- React re-renders (virtual DOM updated)
onSubmitAsync is called synchronously — browser hasn't painted yet!
- Only after the first
await in onSubmitAsync does the browser get a chance to paint
Root Cause
TanStack Form calls onSubmitAsync synchronously after updating state, without yielding to the browser's paint cycle. React's state update schedules a re-render, but the actual browser paint happens asynchronously. By the time the browser paints, the async operation may already be in progress.
Workaround
Use a double requestAnimationFrame at the start of onSubmitAsync to ensure the browser has painted:
onSubmitAsync: async ({ value }) => {
// Wait for browser to complete paint cycle
await new Promise((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(resolve)
})
})
// Now the spinner is visible
const isValid = await validateApiKey(value.apiKey)
return isValid ? undefined : { form: 'Invalid API key' }
}
Suggested Fix
TanStack Form could yield to the browser's paint cycle after setting isSubmitting = true and before calling async validators. This could be done with:
// Before calling onSubmitAsync
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
Or alternatively:
await new Promise(resolve => setTimeout(resolve, 0))
Environment
- @tanstack/react-form: latest
- React: 18.x / 19.x
- Browser: Chrome, Safari, Firefox (issue is consistent across browsers)
- Also reproduced in Tauri WebView
Related Discussions
Description
When using
onSubmitAsyncfor form validation, theisSubmittingstate is set totruecorrectly, but the browser doesn't have time to paint the updated UI (e.g., a loading spinner) before the async validator function starts executing.Steps to Reproduce
onSubmitAsyncvalidatorisSubmittingto show a spinneronSubmitAsync, immediately call an async operation (e.g., API validation)Expected Behavior
The spinner should be visible immediately when the user clicks submit, before
onSubmitAsyncstarts executing.Actual Behavior
The spinner appears late (or not at all for fast operations) because:
handleSubmit()setsisSubmitting = trueonSubmitAsyncis called synchronously — browser hasn't painted yet!awaitinonSubmitAsyncdoes the browser get a chance to paintRoot Cause
TanStack Form calls
onSubmitAsyncsynchronously after updating state, without yielding to the browser's paint cycle. React's state update schedules a re-render, but the actual browser paint happens asynchronously. By the time the browser paints, the async operation may already be in progress.Workaround
Use a double
requestAnimationFrameat the start ofonSubmitAsyncto ensure the browser has painted:Suggested Fix
TanStack Form could yield to the browser's paint cycle after setting
isSubmitting = trueand before calling async validators. This could be done with:Or alternatively:
Environment
Related Discussions