Home/Learn/React/Validation — Rules, Timing and Messages

Validation — Rules, Timing and Messages

Intermediate
Forms

Define the rules once as a schema, validate on blur rather than on every keystroke, and never treat client validation as security.

Overview

Validation has three separable questions and people usually only think about the first. What are the rules — which should live in one schema rather than scattered through handlers. When do you tell the user — because validating on the first keystroke means shouting "invalid email" at someone who has typed the letter a. And where does the truth live — on the server, always, because client validation is a convenience for honest users and no obstacle at all to anyone else.

One Schema

A schema is a single definition producing both the checks and, in TypeScript, the type.

Zod: rules, messages and types in one place
import { z } from 'zod'

const SignupSchema = z.object({
  name: z.string().min(2, 'Please enter your name'),
  email: z.string().email('That does not look like an email'),
  password: z.string()
    .min(8, 'At least 8 characters')
    .regex(/[0-9]/, 'Include a number'),
  confirm: z.string(),
  age: z.coerce.number().int().min(13, 'You must be 13 or older'),
}).refine(v => v.password === v.confirm, {
  message: 'Passwords do not match',
  path: ['confirm'],                 // attaches the error to the right field
})

type SignupValues = z.infer<typeof SignupSchema>    // the type, for free

// Validating gives you either the parsed values or field errors
const result = SignupSchema.safeParse(values)
if (!result.success) setErrors(result.error.flatten().fieldErrors)

// The same schema can run on a Node backend, which is the strongest
// version of "define the rules once".

When to Show an Error

The timing rule that makes a form feel helpful rather than hostile.

Blur first, then change; announce with aria
// Hostile: validate on every keystroke from the first character
// The user sees "Invalid email" while typing "a", "ak", "aks"…

// The convention that works:
//   - validate a field on BLUR (they have finished with it)
//   - after it has errored once, re-validate on CHANGE so the error
//     clears as soon as they fix it
//   - validate everything on SUBMIT
//   - move focus to the first field with an error

const [touched, setTouched] = useState({})
const showError = (field) => touched[field] && errors[field]

<input
  name="email"
  onBlur={() => setTouched(t => ({ ...t, email: true }))}
  aria-invalid={!!showError('email')}
  aria-describedby={showError('email') ? 'email-error' : undefined}
/>
{showError('email') && <p id="email-error" role="alert">{errors.email}</p>}

// aria-describedby is what makes a screen reader announce the error
// with the field. Without it the message is visually present and
// functionally invisible.

Client Validation Is Not Security

What each layer is actually for.

Map 422 responses back onto fields
// Client validation  -> fast feedback, fewer round trips. A
//                       convenience, trivially bypassed with DevTools.
// Server validation   -> the actual rule. Never optional.
// Database constraint -> the last line, for what must never be wrong.

// So the server can always reject, and the UI must handle it by
// mapping errors back onto fields:
const res = await fetch('/api/signup', { method: 'POST', body })
if (res.status === 422) {
  const { errors } = await res.json()      // { email: 'Already registered' }
  setErrors(errors)
  return                                    // keep the user's input
}

// Errors only the server can know — an email already registered, a
// coupon already used, a slot just taken — will always arrive this
// way, so the mapping is required regardless of client validation.

// Write the error messages in the user's terms:
'password_policy_violation'                 // never show this
'Include at least one number'               // show this

Key Points to Remember

  • 1Define validation rules once as a schema, which in TypeScript also produces the values type
  • 2Validate on blur, then on change once a field has already errored, and everything on submit
  • 3Move focus to the first invalid field on submit, and link messages with aria-describedby
  • 4Client validation is convenience only — the server rule is the real one and can always reject
  • 5Map server 422 errors back onto individual fields and never clear the user's input on failure

Interview Questions

Sign in to ask Aria
1

Why validate on blur rather than on every keystroke?

Easy
2

How do you make a validation message accessible to a screen-reader user?

Medium
3

If the client already validates, why does the server still have to?

Easy

Ask Aria about Validation — Rules, Timing and Messages

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…