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?
answer
- state lives in the owner
- context re-renders consumers
- own value, error, touched
- dependent fields go stale
basics
~20 sFormik 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 sEach `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 linesimport { 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
Know that every keystroke updates the form owner's state, and that FastField exists to skip unrelated re-renders.
Explain FastField's update check and why whole-form validation on change adds a second render.
Profile a slow form, apply FastField to independent fields only, and catch the stale dependent-field bug in review.
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