Home/Learn/React/Submission — Pending, Success and Failure

Submission — Pending, Success and Failure

Intermediate
Forms

What happens between the click and the outcome is where forms are won or lost: no double submits, no lost input, and a result the user can actually see.

Overview

The submit handler is the moment the user commits, and it deserves more care than it usually gets. A button that stays enabled during the request creates duplicate records the moment someone double-clicks. A form that clears itself on failure loses work and reads as contempt. A success that produces no visible change makes people submit again. None of this is difficult — it is a checklist — but reviewers notice immediately when it is missing, and so do users.

The Pending State

Disable, label, and keep the layout stable.

Disable, relabel, and hold the layout
<button type="submit" disabled={isSubmitting} aria-busy={isSubmitting}>
  {isSubmitting ? 'Saving…' : 'Save changes'}
</button>

// Disabled alone is not enough feedback. A label change or spinner
// tells the user the click registered.

// Reserve the width so the button does not resize between the two
// labels, which reads as a jitter:
<span className="min-w-[8ch] inline-block text-center">…</span>

// Prevent the other paths to a double submit:
//   - Enter in a text field submits the form too
//   - a second click during the request
// Both are covered by disabling on isSubmitting, provided the flag
// is set before the await:
setSubmitting(true)
try { await save(values) } finally { setSubmitting(false) }

// For anything that creates a record, an idempotency key makes the
// duplicate harmless even if one slips through.

Success

Say what happened, and move the user forward.

Confirm, refresh, and move on
async function onSubmit(values) {
  const created = await createProblem(values)

  toast.success('Problem published')            // it happened
  queryClient.invalidateQueries({ queryKey: ['problems'] })   // the list updates
  navigate(`/problems/${created.slug}`)         // where next
}

// Pick the pattern from the context:
//   creating something  -> navigate to it
//   editing in place    -> stay, show "Saved", reset dirty state
//   a modal             -> close it, and update the list behind
//   a multi-step flow   -> advance to the next step

// reset(values) after an edit clears isDirty, which is what makes
// the unsaved-changes warning stop firing:
reset(created)

// A toast that vanishes in two seconds is not enough on its own for
// something important — reflect the change in the UI as well.

Failure

The rules, and the one that matters most.

Keep the input, name the problem, restore focus
// 1. NEVER clear the form. The user's input is theirs.
// 2. Say which field, if the server told you which field.
// 3. Say what to do next.
// 4. Re-enable the button.

catch (err) {
  if (err.status === 422) return applyFieldErrors(err)
  if (err.status === 409) return setError('root', {
    message: 'Someone else changed this while you were editing. Reload to see their version.',
  })
  if (err.status === 429) return setError('root', {
    message: 'Too many attempts. Try again in a minute.',
  })
  setError('root', { message: 'We could not save that. Please try again.' })
  reportError(err)                     // the detail goes to logging
}

// Move focus to the error so it is announced, not just displayed:
useEffect(() => {
  if (errors.root) rootErrorRef.current?.focus()
}, [errors.root])

// Draft protection for long forms — a lost essay is unforgivable
useEffect(() => {
  const id = setTimeout(() => localStorage.setItem(key, JSON.stringify(values)), 1000)
  return () => clearTimeout(id)
}, [values])

Key Points to Remember

  • 1Disable and relabel the submit button while the request is in flight, setting the flag before the await
  • 2Enter in a text field submits too — disabling on pending covers every path to a double submit
  • 3On success, confirm what happened, refresh the affected data, and move the user forward
  • 4On failure never clear the form, map errors to fields where possible, and re-enable the button
  • 5Move focus to the error message so screen-reader users hear it, and autosave drafts for long forms

Interview Questions

Sign in to ask Aria
1

How do you prevent a form being submitted twice?

Easy
2

What should happen to the user's input when a submission fails?

Easy
3

How would you handle a 409 conflict on an edit form?

Hard

Ask Aria about Submission — Pending, Success and Failure

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…