React

State in a multi-step form

A multi-step form keeps one state object and shows one step at a time: if each step owns its useState, going back loses what was already typed.

Key points

  • State lives in the container of the steps, not in each step.
  • Going back is free when the values are never unmounted.
  • The Next button validates only the fields of the visible step.

A multi-step form keeps one state object for all steps and shows one at a time: if each step owns its useState, going back loses what was already typed.

One state, many steps

Form state lives in the component that holds the steps, not in the steps themselves. A step receives the values and an update function, and knows neither the previous step nor the next one.

const [step, setStep] = useState(0);
const [values, setValues] = useState({ name: "", email: "", plan: "base" });

const update = (field) => (event) =>
  setValues((current) => ({ ...current, [field]: event.target.value }));

return step === 0 ? (
   setStep(1)} />
) : (
   setStep(0)} />
);

With this shape, going back costs nothing: the values were never unmounted, so the previous step finds them unchanged, cursor position aside.

Validate where you advance, not where you type

  • One error per step: the Next button checks only the visible fields, so the message is about what the reader is looking at.
  • No validation on mount: an empty step shows no error the moment it appears, otherwise it looks wrong from the start.
  • Derived summary: the last step reads the same object and does not rebuild the data.

The project in four commands

npm create vite@latest multi-step -- --template react
cd multi-step
npm install
npm run dev

Vite, under the MIT licence, creates the project; steps are added as components in src/ and the state stays in a single file. Render time was measured in the React DevTools Profiler, step by step.

Live records in the catalogue

Three implementations to read: multi-step-form, multistep-form and registration-stepper. To browse by category start from form blocks.

The limitation

A single state object re-renders the whole form on every keystroke, because the object changes identity. At fifty fields, split the state per step or move to a library with per-field subscriptions, and accept that the summary reads from several sources.

Where to continue

The full pattern is in multi-step form with shadcn; field handling is in react-hook-form with shadcn; the comparison between controlled and uncontrolled state is in controlled and uncontrolled forms.

Frequently asked questions

Where should form state live?

In the component that holds the steps. When each step keeps its own state, the value disappears as the step unmounts.

One object or one useState per field?

One object while the fields are few, because the summary reads a single source. Past roughly thirty fields, split it.

When do you validate a step?

When the reader tries to advance, not while typing and not on mount. That way the error is about an action just taken.

How do you save a draft?

By serialising the state object into sessionStorage or to the server. React state alone disappears on refresh.

Does a hidden step stay in the DOM?

It depends on conditional rendering: unmounting the step loses focus and local state, while the shared values survive.