Validate before submit

Controlled fields, a per-field error map, and a guarded submit.

From frontend-patterns, “Controlled Form with Validation”.

Submit an empty form

Press Submit both, then fill the fields and retry.

Server decidesBad
requests: 0
validate() firstGood
requests: 0errors: {}
const validate = (): boolean => {
  const newErrors: FormErrors = {}
  if (!formData.name.trim()) newErrors.name = 'Name is required'
  else if (formData.name.length > 200) newErrors.name = 'Name must be under 200 characters'
  if (!formData.description.trim()) newErrors.description = 'Description is required'
  if (!formData.endDate) newErrors.endDate = 'End date is required'
  setErrors(newErrors)
  return Object.keys(newErrors).length === 0
}

const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault()
  if (!validate()) return
  await createMarket(formData)
}

Whitespace and length

Type a name or pick a preset.

0/200
Naive checkBad
if (!name) errors.name = 'Name is required'
trim() + max lengthGood
!name.trim()name.length > 200
if (!formData.name.trim()) {
  newErrors.name = 'Name is required'
} else if (formData.name.length > 200) {
  newErrors.name = 'Name must be under 200 characters'
}

When createMarket throws

Press Create in each, then untick the failure.

await, no catchBad
await createMarket(formData)
try / catch / finallyGood
catch → setSubmitError(…)
setSubmitting(true)
try {
  await createMarket(formData)
} catch (error) {
  setSubmitError('Could not create the market. Try again.')
} finally {
  setSubmitting(false)
}

Why it matters, pros and cons

Why it matters

A client check is instant and names the field. A caught submit becomes UI state.

Pros

  • Every error shows beside its field.
  • No requests the server would reject.
  • validate() is pure and easy to test.

Cons

  • Not security: the server must validate again.
  • Errors refresh only on submit unless you revalidate on change.
  • Native required may be enough for one field.