skip to content

A 150-field Formik 2 form lags while typing because every keystroke re-renders every field — why, and when does FastField help or go stale?

level: seniorimportance: should knowfreq 30%

answer

  1. state lives in the owner
  2. context re-renders consumers
  3. own value, error, touched
  4. dependent fields go stale

basics

~20 s

Formik 2 keeps values in the form owner's state, so each keystroke re-renders the owner and every Field under it. FastField re-renders only when its own value, error or touched, or isSubmitting, changes — so it goes stale if it depends on another field.

solid answer

~40 s

Each `handleChange` updates the state held by the component running `useFormik` or `<Formik>`, which re-renders that owner and, through context, every `<Field>`, `useField` component and render-prop child. With `validateOnChange: true` (the default) the whole form is validated after the change, and a changed `errors` object causes another render. At 150 fields that is 150 components per keystroke, often twice. `<FastField>` is a `<Field>` with a `shouldComponentUpdate` that re-renders only when its `name`, its own slice of `values`, `errors` or `touched`, the number of its props, or `isSubmitting` changes. That makes independent fields cheap, but a field whose rendering depends on **another** field — options filtered by `values.country` — stops updating unless you pass a `shouldUpdate` function. Other levers: `validateOnChange={false}`, splitting the form into sections, and keeping heavy widgets out of the render-prop child.

code

tsx · 34 lines
tsx
import { FastField, Field, Form, Formik } from 'formik';

type Address = { street: string; city: string; country: string; region: string };
const REGIONS: Record<string, string[]> = { DE: ['Bavaria', 'Berlin'], FR: ['Brittany', 'Normandy'] };

export function AddressForm({ onSave }: { onSave: (a: Address) => Promise<void> }) {
  return (
    <Formik<Address>
      initialValues={{ street: '', city: '', country: 'DE', region: '' }}
      validateOnChange={false}
      onSubmit={onSave}
    >
      {({ values }) => (
        <Form>
          {/* independent fields: skip re-renders caused by other fields */}
          <FastField name="street" />
          <FastField name="city" />
          <Field name="country" as="select">
            <option value="DE">Germany</option>
            <option value="FR">France</option>
          </Field>
          {/* depends on values.country: a plain Field keeps its options current */}
          <Field name="region" as="select">
            <option value="">Choose a region</option>
            {REGIONS[values.country].map((r) => (
              <option key={r} value={r}>{r}</option>
            ))}
          </Field>
          <button type="submit">Save</button>
        </Form>
      )}
    </Formik>
  );
}

go deeper

for a junior

Know that every keystroke updates the form owner's state, and that FastField exists to skip unrelated re-renders.

for a middle

Explain FastField's update check and why whole-form validation on change adds a second render.

for a senior

Profile a slow form, apply FastField to independent fields only, and catch the stale dependent-field bug in review.

for a principal

Decide when a form has outgrown a controlled single-owner model and what the migration or splitting plan should be.

## Where the cost comes from Formik 2 is a **controlled** form library: the component that runs `useFormik` — including the `<Formik>` component — holds `values`, `errors` and `touched`, and every input's `value` comes from there. In 2.4.9 the state sits in a ref updated by a reducer, and each update bumps a counter to force the owner to re-render. On one keystroke in a large form: 1. `handleChange` updates `values`, forcing the owner to re-render. 2. The owner's render-prop child re-renders, and the field consumers — `<Field>`, components using `useField` or `useFormikContext` — re-render with it. (`<ErrorMessage>` is an exception: it carries its own narrow update check on its field's error and touched.) 3. With `validateOnChange` at its default `true`, Formik validates the **whole form** and, if `errors` changes, updates state again, causing a second round. With 150 fields, some wrapping heavy widgets, that is enough to drop frames while typing. ## What FastField changes `<FastField>` has the same API as `<Field>` but is implemented as a class with a custom `shouldComponentUpdate`. By default it re-renders only when: - its `name` prop changes; - `values[name]`, `errors[name]` or `touched[name]` changes; - the **number** of props passed to it changes; - `formik.isSubmitting` changes. Everything else — keystrokes in other fields, other fields' errors — is skipped. For a long form of mostly independent fields, that turns "every field per keystroke" into "the field being typed in, plus the owner". ## When FastField goes stale The same check is FastField's trap. Suppose a `region` select's options depend on `values.country`: ```tsx <FastField name="region" as="select"> {regionsFor(values.country).map((r) => <option key={r}>{r}</option>)} </FastField> ``` When `country` changes, `region`'s own value, error and touched are unchanged, so FastField blocks the re-render and keeps showing the old country's options. Fixes: - use a plain `<Field>` for dependent fields; - pass `shouldUpdate={(next, prev) => next.formik.values.country !== prev.formik.values.country || ...}` to add the dependency; - restructure so the dependency arrives as a prop — but note FastField compares the **count** of props, not their values. ## Other levers, in the order to try them | Lever | What it saves | Trade-off | |---|---|---| | `<FastField>` for independent fields | Re-renders of untouched fields | Stale dependent fields | | `validateOnChange={false}` | The whole-form validation per keystroke and its extra render | Errors refresh on blur and submit only | | Memoise heavy widgets outside the render-prop child | Expensive subtrees | More components to maintain | | Split into steps or sections with separate forms | The size of each re-render | Cross-section validation needs care | Profile first: record a React profiler trace while typing, and check whether the time goes to many cheap field renders, a few expensive widgets, or schema validation. ## Judging it in an interview A strong answer explains the mechanism — controlled values in one owner, re-rendered through context — rather than calling the library "slow". It knows that FastField's check covers the field's own `values`, `errors` and `touched` plus `isSubmitting`, and it names the stale-dependency trap without being prompted. A weak answer suggests wrapping every field in `React.memo`, which does not stop context consumers from re-rendering.

  • In Formik 2, how do you keep a FastField but still let it react to another field?
    Pass a `shouldUpdate` prop. It replaces FastField's default check and receives the next and previous props, including `formik`, so `shouldUpdate={(next, prev) => next.formik.values.country !== prev.formik.values.country}` re-renders when `country` changes. Remember to also include the field's own value, or it stops updating when typed into.
  • Why does wrapping each Formik 2 field component in React.memo not fix the re-renders?
    Components that read Formik's context — `<Field>`, `useField`, `useFormikContext` — re-render when the context value changes, and it changes on every keystroke. `React.memo` compares props only, so it cannot block a context-driven re-render. FastField works because its inner class receives the Formik state as a prop and compares specific slices of it.

A Formik form is a team chat where every message pings all 150 members; FastField mutes a member except for messages that mention them, which is fine until someone needs to hear about a message that never names them.

saying these in an interview costs you the question

  • Formik only re-renders the field being typed into
  • FastField re-renders whenever any value in the form changes
  • React.memo on each field stops context-driven re-renders
  • FastField is always safe to use instead of Field
  • Turning off validateOnChange stops values from updating on each keystroke