3.5 KiB
3.5 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Wrap Async Submit Handlers in try/catch and Reset on isSubmitSuccessful | HIGH | prevents stuck isSubmitting state and missing post-success reset | formstate, isSubmitting, isSubmitSuccessful, async, submit, reset |
Wrap Async Submit Handlers in try/catch and Reset on isSubmitSuccessful
isSubmitting is the canonical way to disable the submit button while a request is in flight, but it has a well-known footgun: if your submit handler throws, isSubmitting stays true and the form becomes unrecoverable. Always try/catch inside the async handler. Pair this with isSubmitSuccessful + useEffect(reset) to clear the form after a successful submit (resetting inside the handler races with the success state transition).
Incorrect (throw leaves isSubmitting stuck; manual reset races):
function CreatePostForm() {
const { register, handleSubmit, reset, formState: { isSubmitting } } = useForm<PostFormData>()
const onSubmit = async (data: PostFormData) => {
const res = await fetch('/api/posts', { method: 'POST', body: JSON.stringify(data) })
if (!res.ok) throw new Error('Save failed') // isSubmitting will stay true forever
reset() // Races with the form's success state
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('title')} />
<textarea {...register('body')} />
<button disabled={isSubmitting}>{isSubmitting ? 'Saving…' : 'Save'}</button>
</form>
)
}
Correct (try/catch keeps form recoverable; useEffect resets after success):
function CreatePostForm() {
const {
register,
handleSubmit,
reset,
setError,
formState: { isSubmitting, isSubmitSuccessful, errors },
} = useForm<PostFormData>()
const onSubmit = async (data: PostFormData) => {
try {
const res = await fetch('/api/posts', { method: 'POST', body: JSON.stringify(data) })
if (!res.ok) {
setError('root.serverError', { type: 'server', message: 'Save failed' })
}
} catch {
setError('root.serverError', { type: 'network', message: 'Network error — please retry' })
}
}
// Reset after a successful submit completes — runs once per success transition
useEffect(() => {
if (isSubmitSuccessful) reset()
}, [isSubmitSuccessful, reset])
return (
<form onSubmit={handleSubmit(onSubmit)}>
{errors.root?.serverError && <div role="alert">{errors.root.serverError.message}</div>}
<input {...register('title')} />
<textarea {...register('body')} />
<button disabled={isSubmitting}>{isSubmitting ? 'Saving…' : 'Save'}</button>
</form>
)
}
Key details:
isSubmittingresets only when the handler returns (resolves). A throw leaves ittrueand the form unrecoverableisSubmitSuccessfulbecomestruewhen the handler completes without throwing and without callingsetError. Use it to gate the post-success reset- Calling
reset()inside the submit handler races with React's commit ofisSubmitSuccessful; theuseEffectform is the documented pattern - If you want to preserve specific fields across reset, pass them:
reset(undefined, { keepDirtyValues: true })orreset({ defaultValue: lastSaved })
Reference: formState · reset · Discussion #10103 — isSubmitting does not recover when submit handler throws