A Formik 2 form's submit button, disabled by isSubmitting, stays disabled forever after the first submit — what in onSubmit causes that?
answer
- sync versus promise
- who resets the flag
- the pre-submit phase
- submitCount and touched
basics
~10 sFormik 2 resets isSubmitting only when onSubmit returns a promise, once it settles. A synchronous onSubmit, or one that starts a request without returning it, leaves isSubmitting true until you call setSubmitting(false).
solid answer
~40 sOn `handleSubmit` or `submitForm`, Formik first touches every field present in `values`, sets `isSubmitting` to `true` and increments `submitCount`; then it runs all validation. If there are errors, it sets `isSubmitting` back to `false` and never calls `onSubmit`. If there are none, it calls `onSubmit(values, formikBag)`. If that returns a promise, Formik sets `isSubmitting` to `false` when it resolves or rejects. If it returns `undefined` — a synchronous handler, or `fetch(...).then(...)` without `return` — Formik assumes you will finish the cycle yourself with `formikBag.setSubmitting(false)`. Forgetting that leaves the button disabled. The fix is to make `onSubmit` `async` and `await` the request, or to call `setSubmitting(false)` in every path.
code
tsx · 27 linesimport { Field, Form, Formik } from 'formik';
type Comment = { comment: string };
export function CommentForm({ post }: { post: (c: Comment) => Promise<void> }) {
return (
<Formik<Comment>
initialValues={{ comment: '' }}
onSubmit={async (values, { resetForm, setStatus }) => {
try {
await post(values); // awaited: Formik resets isSubmitting when this settles
resetForm();
} catch {
setStatus('Could not post your comment. Try again.');
}
}}
>
{({ isSubmitting, status }) => (
<Form>
<Field name="comment" as="textarea" />
{status && <p role="alert">{status}</p>}
<button type="submit" disabled={isSubmitting}>Post</button>
</Form>
)}
</Formik>
);
}go deeper
Remember the rule: return or await the request in onSubmit, or call setSubmitting(false) yourself.
Walk through the three submit phases and explain why Formik leaves the flag alone for synchronous handlers.
Handle rejections inside onSubmit with setStatus or setFieldError, keep initialValues complete so submit touches every field, and gate errors on submitCount where needed.
Standardise an async submit contract for shared form components so every team gets consistent pending and error behaviour.
## The symptom ```tsx <Formik initialValues={{ comment: '' }} onSubmit={(values) => { api.post('/comments', values).then(showToast); // no return }} > {({ isSubmitting }) => ( <Form> <Field name="comment" as="textarea" /> <button type="submit" disabled={isSubmitting}>Post</button> </Form> )} </Formik> ``` The first click posts the comment. The button then stays disabled for the life of the form. ## What Formik does on submit, in order The Formik submission guide spells out three phases for `handleSubmit(e)` or `submitForm()`: **Pre-submit** 1. Touch all fields — every key present in `values` is marked `true` in `touched`. 2. Set `isSubmitting` to `true`. 3. Increment `submitCount`. **Validation** 4. Set `isValidating` to `true` and run field-level `validate` functions, the `validate` prop and the `validationSchema`, deeply merging the results. 5. If there are errors: set `errors`, set `isSubmitting` to `false`, and **stop** — `onSubmit` is not called. **Submission** 6. Call `onSubmit(values, formikBag)`. 7. If it returned a **promise**, wait for it to resolve or reject, then set `isSubmitting` to `false`. 8. If it returned **nothing**, do nothing more: you are expected to call `setSubmitting(false)`. `handleSubmit` also calls `preventDefault()` on the event, so the page does not reload. ## Why step 8 exists Formik 1 code commonly used callbacks as side effects inside `onSubmit`. If Formik 2 always reset `isSubmitting` right after a synchronous `onSubmit` returned, the flag would drop to `false` before those callbacks finished, and a disabled button would be useless. So Formik treats "no promise" as "the consumer owns the flag". The source comment says so directly: a synchronous handler must clean up with `setSubmitting(false)`. In the symptom above, `onSubmit` starts a promise but does not **return** it, so Formik sees `undefined` and never resets the flag. ## Fixes | Approach | How | Note | |---|---|---| | Return the promise | `onSubmit={(v) => api.post(...)}` | Formik resets on settle | | `async`/`await` | `onSubmit={async (v) => { await api.post(...) }}` | Clearest; errors reject the promise | | Manual reset | call `setSubmitting(false)` in `then` and `catch` | Needed for truly callback-based code | With a returned promise, a **rejection** still resets `isSubmitting`, and the rejection is re-thrown to the caller of `submitForm`. Catch it inside `onSubmit` if you want to show a server error with `setStatus` or `setFieldError` instead. ## The touched detail in pre-submit Step 1 touches the keys present in `values`. A field that was left out of `initialValues` and never changed has **no key**, so it is not touched on submit, and its error stays hidden behind the touched gate even though validation failed. This is why the submission guide says `initialValues` are required and should always be specified. ## Other flags worth knowing - **`isValidating`** is `true` during step 4; the docs say the submission handler is executing when `isSubmitting` is `true` and `isValidating` is `false`. - **`submitCount`** increments on every attempt, valid or not, which makes it a good gate for "show errors after the first submit". - **`formikBag`** passed to `onSubmit` carries helpers such as `setSubmitting`, `setFieldError`, `setStatus` and `resetForm`, but not `errors` or `touched`.
- In Formik 2, is onSubmit called when validation fails on submit?No. Formik touches all fields, sets `isSubmitting`, validates, and if the merged errors object is not empty it sets `errors`, sets `isSubmitting` back to `false` and stops. `onSubmit` runs only for a valid form, so it never needs to re-check the validation rules.
- How would you show a server-side error for one field after a Formik 2 submit?Inside `onSubmit`, catch the failed request and call `setFieldError('email', 'Already registered')` from the formik bag, or `setStatus(...)` for a form-wide message. Because `touched` was set for every field in the pre-submit phase, the field's error displays straight away through `<ErrorMessage>`.
saying these in an interview costs you the question
- Formik always sets isSubmitting to false after onSubmit returns
- onSubmit runs even when validation fails, with errors in the bag
- A request started inside onSubmit is tracked even without returning it
- Submit only touches the fields the user focused
- isSubmitting stays true only while validation is running