How to Async Submit with Formik
Handle async submission in Formik so the submit button shows a loading state, errors map to fields, and double submit is prevented by isSubmitting.
On this page
Introduction
The hardest part of handle async form submit is not writing the code — it is deciding what the code is allowed to reject. A Formik setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in Formik, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.
The cost of handle async form submit without Formik is paid in incidents, not in code. The code looks fine because the happy path works, and the happy path is what you demo. The failure path is what you operate, and the failure path is where Formik earns its keep. Instrument the rejections, alert on the spikes, and let the metric tell you when the guard is misaligned with reality.
The sections below walk through the concrete decisions: where to put the formik check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.
Define the submit handler
Define the submit handler is the first decision because it determines what the rest of the Formik pipeline can assume. A wrong choice here turns into handle async form submit under load, and the invariant you thought Formik guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Define the submit handler is the one most teams skip: drive formik to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
<Formik onSubmit={async (vals, { setErrors, setSubmitting }) => {
try { await api.post('/x', vals); }
catch (e) { setErrors(mapErrors(e)); }
finally { setSubmitting(false); }
}}>…</Formik>
The javascript above is the shape to ship for handle async form submit. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.
Map server errors to fields
The mechanism behind Map server errors to fields is straightforward in Formik, but the consequences ripple. A wrong choice here turns into handle async form submit under load, and the invariant you thought Formik guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Map server errors to fields is the one most teams skip: drive formik to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Guard with isSubmitting
Guard with isSubmitting is where most Formik implementations leak performance. A wrong choice here turns into handle async form submit under load, and the invariant you thought Formik guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Guard with isSubmitting is the one most teams skip: drive formik to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
test('maps server errors', async () => {
api.post.mockRejectedValue({ field: 'email', msg: 'taken' });
await submit();
expect(errors.email).toBe('taken');
});
Pin the behavior with this test. A Formik setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.
Reset on success
The hard part of Reset on success is not the happy path — it is what happens when formik fails halfway. A wrong choice here turns into handle async form submit under load, and the invariant you thought Formik guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Reset on success is the one most teams skip: drive formik to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Show loading state
Show loading state should be observable without a debugger. A wrong choice here turns into handle async form submit under load, and the invariant you thought Formik guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Show loading state is the one most teams skip: drive formik to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Test the async flow
Test the async flow is a trade, not a winner. A wrong choice here turns into handle async form submit under load, and the invariant you thought Formik guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Test the async flow is the one most teams skip: drive formik to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Common pitfalls
The common pitfalls in handle async form submit all reduce to one mistake: treating Formik as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.
- Skipping the formik boundary so handle async form submit enters unvalidated.
- Assuming Formik is fast without measuring the tail latency under load.
- Letting handle async form submit fail silently instead of failing loudly with a metric.
- Testing only the happy path so the formik failure surfaces in production.
- Sharing mutable state across requests so one bad input poisons the next.
Conclusion
Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: Formik fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.
Self-check: write a test that drives formik to the failing condition and assert the system degrades gracefully. Then handle async form submit is a property you maintain, not a feature you hope for.
FAQ
- How prevent double submit?
- Disable the button while isSubmitting is true. Formik sets it automatically during onSubmit.
- Should I use Formik or RHF?
- RHF for new work — fewer re-renders. Formik if you already have it and the bundle is not a concern.
Report a problem with this articleOpens a pre-filled issue on GitHub.