skip to content

In Formik 2, a field's error already exists in errors while the user types, yet no message shows until they leave the field — why?

level: middleimportance: should knowfreq 40%

answer

  1. errors and touched are separate
  2. validation is form-wide
  3. blur sets touched
  4. the display gate

basics

~20 s

Formik 2 validates the whole form on every change and blur by default, so errors fills in while the user types. touched.field is set only by blur or submit, and the usual display gate — including ErrorMessage — needs both.

solid answer

~40 s

Formik keeps two separate maps shaped like `values`: `errors` (what validation says now) and `touched` (which fields the user has left). With the defaults `validateOnChange: true` and `validateOnBlur: true`, every `handleChange`, `setFieldValue` or `setValues` runs **form-level** validation — the `validate` function, the Yup `validationSchema` and any field-level `validate`, deeply merged — so `errors.email` appears as soon as the value is wrong. But `touched.email` becomes `true` only through `handleBlur`, `setFieldTouched`, `setTouched` or a submit, which touches every field present in `values`. `<ErrorMessage>` and the documented pattern `touched.email && errors.email` show the message only when both are set, which is why it waits for blur. Turning off `validateOnChange` changes when `errors` is computed, not when it is shown.

code

tsx · 30 lines
tsx
import { Field, Form, Formik } from 'formik';
import * as Yup from 'yup';

const schema = Yup.object({
  username: Yup.string().min(3, 'At least 3 characters').required('Required'),
  password: Yup.string().min(12, 'At least 12 characters').required('Required'),
});

export function AccountForm({ onSave }: { onSave: (v: { username: string; password: string }) => Promise<void> }) {
  return (
    <Formik
      initialValues={{ username: '', password: '' }}
      validationSchema={schema}
      validateOnChange={false} // errors refresh on blur and submit only
      onSubmit={onSave}
    >
      {({ errors, touched, submitCount }) => (
        <Form>
          <Field name="username" />
          {touched.username && errors.username && <p>{errors.username}</p>}

          <Field name="password" type="password" />
          {(touched.password || submitCount > 0) && errors.password && <p>{errors.password}</p>}

          <button type="submit">Save</button>
        </Form>
      )}
    </Formik>
  );
}

go deeper

for a junior

Know that errors and touched are separate, and that ErrorMessage shows an error only for a touched field.

for a middle

List what triggers validation and touching, the two defaults, and why validation runs across the whole form.

for a senior

Tune validateOnChange for large forms, make custom controls report blur, and choose a display gate that matches the product's error policy.

for a principal

Set one error-display policy for a product's forms and make shared field components enforce it consistently.

## Two maps, one display rule Formik 2 tracks field status in two objects that mirror the shape of `values`: - **`errors`** — the latest validation result, e.g. `{ email: 'Invalid email' }`. - **`touched`** — which fields the user has interacted with and left, e.g. `{ email: true }`. They are updated by different events, and Formik deliberately keeps them apart. The documented display rule combines them: ```tsx {touched.email && errors.email ? <p>{errors.email}</p> : null} ``` `<ErrorMessage name="email" />` implements exactly that rule: it renders only when both `touched.email` and `errors.email` are truthy. ## When validation runs The Formik docs list the triggers: | Trigger | Controlled by | Default | |---|---|---| | `handleChange`, `setFieldValue`, `setValues` | `validateOnChange` | `true` | | `handleBlur`, `setFieldTouched`, `setTouched` | `validateOnBlur` | `true` | | `handleSubmit`, `submitForm` | always | — | | `validateForm`, `validateField` | manual | — | | mount, or `initialValues` changing | `validateOnMount` | `false` | Two properties of this are easy to miss: 1. **Validation is form-wide.** A change to one field runs the `validate` function or `validationSchema` over **all** values, then merges field-level `validate` results. So typing in `email` can also produce `errors.password` — hidden, because `password` is not touched. 2. **The setters take a `shouldValidate` override.** `setFieldValue(field, value, false)` skips validation for that call even when `validateOnChange` is `true`. ## When a field becomes touched `touched.email` is set by: - `handleBlur` on the input, which `<Field>` wires for you; - `setFieldTouched('email', true)` or `setTouched(...)` from custom components; - a submit, which touches every field present in `values` before validating. Typing never touches a field. That is the whole answer to the question: the error exists during typing, the gate opens on blur. ## validate versus validationSchema - **`validate(values)`** returns an errors object (or a promise of one). Leave a key out when the field is valid; the Formik FAQ says not to use `null` for "no error", because computed props such as `isValid` then misbehave. - **`validationSchema`** takes a Yup schema. Before validating, Formik converts empty strings in `values` to `undefined`, so Yup's `required()` treats an empty text field as missing. Yup errors are mapped back onto keys that match `values`. - Both can be combined with field-level `validate` props; the results are deeply merged. ## Consequences in practice - **`isValid` can be `true` on a pristine form.** It checks that `errors` is empty, and nothing has validated yet unless you set `validateOnMount` or `initialErrors`. - **Custom inputs must call `setFieldTouched`** or wire `onBlur`, or their errors never appear until submit. - **Turning off `validateOnChange`** reduces validation work on large forms; errors then refresh on blur and submit, and the touched gate still decides visibility. - **Showing errors on submit only** is a display choice: gate on `submitCount > 0` instead of, or as well as, `touched`.

  • Why is isValid true on a freshly mounted Formik 2 form whose required fields are empty?
    `isValid` checks that the `errors` object has no keys, and nothing has validated yet: `validateOnMount` defaults to `false`. Set `validateOnMount` to run validation on mount, or pass `initialErrors`, if a submit button must start disabled.
  • In Formik 2, a custom slider updates its value with setFieldValue but its error never appears before submit. What is missing?
    The field is never touched. A custom control must report blur — call `setFieldTouched('volume', true)` when the user finishes interacting, or pass `field.onBlur` through. Without that, `touched.volume` stays unset until a submit touches every field, so the touched gate hides the error.
  • What does Formik 2 do to empty strings before running a validationSchema?
    It converts `''` to `undefined` throughout the values it passes to Yup (the `prepareDataForValidation` step), so `Yup.string().required()` fails on an empty field. The values in your form state are not changed; only the copy handed to the schema is.

saying these in an interview costs you the question

  • Formik validates only the field that changed
  • errors stays empty until the field has been touched
  • Typing in a field marks it as touched
  • Setting validateOnChange to false hides errors until submit
  • Return null in validate for a field that has no error