skip to content

A Formik 2 edit-profile form renders before the user record loads and stays empty after it arrives — how do you fix it, and what does enableReinitialize risk?

level: seniorimportance: should knowfreq 35%

answer

  1. initial means read once
  2. deep-equality reset
  3. edits get discarded
  4. remount by key

basics

~20 s

Formik 2 reads initialValues once, at mount. enableReinitialize resets the form whenever initialValues changes by deep equality, which discards unsaved edits if the loaded data changes again; rendering after load or keying by user id avoids that.

solid answer

~40 s

Formik copies `initialValues` into its state on mount and, by default (`enableReinitialize: false`), ignores later changes — so a form mounted with empty placeholders keeps them. Three fixes: **render the form only once the user has loaded**; **remount it with `key={user.id}`** so a different user gets a fresh form; or set **`enableReinitialize`**, which calls `resetForm()` whenever `initialValues` changes by **deep equality** — replacing `values` and resetting `errors`, `touched` and `submitCount`. The risk is that reset: if the loaded record changes while the user is editing — a background refetch bringing a new `updatedAt`, say — the user's edits vanish. Build `initialValues` from only the editable fields, or prefer the render-after-load or key approaches.

code

tsx · 32 lines
tsx
import { useMemo } from 'react';
import { Field, Form, Formik } from 'formik';

type User = { id: string; displayName: string; bio: string; updatedAt: string };
type ProfileValues = { displayName: string; bio: string };

export function EditProfile({ user, save }: { user: User | undefined; save: (v: ProfileValues) => Promise<ProfileValues> }) {
  // Only editable fields: a refetch that changes updatedAt compares equal.
  const initialValues = useMemo<ProfileValues | undefined>(
    () => (user ? { displayName: user.displayName, bio: user.bio } : undefined),
    [user?.displayName, user?.bio],
  );

  if (!user || !initialValues) return <p>Loading profile...</p>;

  return (
    <Formik
      key={user.id} // a different user gets a fresh form
      initialValues={initialValues}
      onSubmit={async (values, { resetForm }) => {
        const saved = await save(values);
        resetForm({ values: saved }); // new baseline, so dirty becomes false
      }}
    >
      <Form>
        <Field name="displayName" />
        <Field name="bio" as="textarea" />
        <button type="submit">Save</button>
      </Form>
    </Formik>
  );
}

go deeper

for a junior

Know that initialValues is read once and that enableReinitialize exists and defaults to false.

for a middle

Explain the deep-equality comparison and that reinitialising resets values, errors, touched and submitCount.

for a senior

Diagnose lost edits caused by background refetches and choose render-after-load, key remounts or editable-only initial values.

for a principal

Set a data-loading pattern for edit screens that keeps server cache refreshes from ever clobbering in-progress edits.

## The symptom ```tsx function EditProfile() { const user = useUser(); // undefined, then the loaded record return ( <Formik initialValues={{ displayName: user?.displayName ?? '', bio: user?.bio ?? '' }} onSubmit={save} > {/* fields */} </Formik> ); } ``` On the first render `user` is `undefined`, so the form mounts with empty strings. When the record arrives, the new `initialValues` is ignored and the fields stay empty. ## Why: initial means initial Formik stores `initialValues` in a ref at mount and seeds its state from it. By default nothing re-reads the prop. This is deliberate: a parent re-rendering with a fresh object should not wipe what the user typed. The `dirty` flag compares current `values` against that stored initial copy with deep equality. ## Three fixes | Fix | How | Effect on user edits | |---|---|---| | Render after load | `if (!user) return <Spinner />;` then render `<Formik>` | Form mounts once with real data; nothing to reset | | Remount by key | `<Formik key={user.id} ...>` | A different user gets a fresh form; the same user keeps edits | | `enableReinitialize` | `<Formik enableReinitialize ...>` | Any deep change to `initialValues` resets the form | ## What enableReinitialize actually does With `enableReinitialize` set (default `false`), Formik runs an effect after each render that compares the new `initialValues` prop with the stored one using **deep equality**. If they differ, it: 1. stores the new object as the initial values; 2. calls `resetForm()`, which replaces `values` and resets `errors`, `touched`, `status`, `isSubmitting` and `submitCount`; 3. re-validates if `validateOnMount` is set. Deep equality matters in both directions. It means an inline object literal, recreated on every parent render, does **not** trigger a reset as long as its contents are the same. It also means **any** content change does — including fields the form does not even display. ## The production trap Edit-profile screens often load through a server-state cache that refetches in the background — on window focus, on an interval, after another mutation. If a refetch returns a record whose contents differ at all — a new `updatedAt`, a counter, an avatar URL — and `initialValues` is built from the whole record, `enableReinitialize` resets the form and the user's unsaved edits disappear mid-typing. Mitigations: - **Build `initialValues` from editable fields only**, so unrelated server changes compare equal. - **Memoise on the fields that matter**, e.g. `useMemo(() => pick(user), [user.id, user.displayName, user.bio])`. - **Prefer render-after-load or `key={user.id}`** when the goal is only "populate once". - **Reset explicitly after saving**, with `resetForm({ values: saved })`, which also makes the saved values the new baseline for `dirty`. ## Related reset tools - `resetForm()` with no argument returns to the current initial values. - `resetForm({ values })` sets new values **and** makes them the new initial values, so `dirty` becomes `false`. - `initialErrors`, `initialTouched` and `initialStatus` are reinitialised the same way when `enableReinitialize` is on. - `validateOnMount` also re-runs validation when `initialValues` change under `enableReinitialize`. ## Judging it in an interview A strong answer names the default (`false`), the deep-equality comparison, and the fact that a reinitialise is a **reset**, not a merge. Proposing `enableReinitialize` without mentioning lost edits is the common weak answer; proposing `useEffect` plus `setValues` on every data change reproduces the same trap by hand.

  • Does passing an inline initialValues object to a Formik 2 form with enableReinitialize reset it on every parent render?
    No. Formik compares the new `initialValues` with the stored one by deep equality, so a new object with the same contents is ignored. Only a change in contents resets the form — which is exactly why including server-managed fields such as `updatedAt` is dangerous.
  • After a successful save in Formik 2, how do you make dirty false without remounting the form?
    Call `resetForm({ values: savedValues })` from the formik bag in `onSubmit`. It sets the values and makes them the new initial values, so `dirty` — a deep comparison of `values` against the initial values — becomes `false`, and `touched` and `errors` are cleared.
  • Why is a useEffect that calls setValues whenever the loaded user changes not a better fix in Formik 2?
    It re-creates the same trap by hand: each data change overwrites what the user typed, and it does not update the initial values, so `dirty` compares against stale data. Rendering after load, keying by id, or `enableReinitialize` with editable-only values are clearer.

saying these in an interview costs you the question

  • Formik picks up new initialValues automatically
  • enableReinitialize merges new values into fields the user has not touched
  • An inline initialValues object resets the form on every render
  • enableReinitialize is safe with any server data because it compares by reference
  • setValues in a useEffect is the recommended way to load data