gform-react
Back to Guides

Multi-Step Forms

Build a wizard with one GForm - validate each step before advancing and preserve values across steps.

A wizard is still one GForm - you just render one step's fields at a time. Two things make it work cleanly with gform-react:

  1. Validate the current step before advancing. state.checkValidity() validates only the fields that are currently mounted (the step on screen) and surfaces their errors - so it's effectively per-step validation.
  2. Preserve values across steps. A GInput unregisters when it unmounts, so leaving a step drops its values from form state. Capture them into React state as you navigate, and re-seed each field from there via its value prop.

The pattern

Keep the step index and the accumulated values in component state. On Next, validate then capture; on Back, just capture. Seed every field's value from the saved data so a revisited step shows what was entered:

const [step, setStep] = useState(0);
const [data, setData] = useState({});
 
// inside the render prop, with access to `state`:
const capture = () => setData((d) => ({ ...d, ...state.toRawData() }));
const next = () => {
  if (state.checkValidity()) {     // validates the mounted step + shows errors
    capture();
    setStep((s) => s + 1);
  }
};
const back = () => {
  capture();
  setStep((s) => s - 1);
};
{step === 0 && (
  <GInput formKey="firstName" required value={data.firstName} element={/* … */} />
)}

On the final step, submit as usual. onSubmit only fires when the (last) step is valid, so merge the saved data with the last step's values:

onSubmit={(state, e) => {
  e.preventDefault();
  const all = { ...data, ...state.toRawData() };
  save(all);
}}
checkValidity covers native constraints

state.checkValidity() runs the native constraints (required, minLength, pattern, type, …). If a step relies on custom or async rules, validate those fields explicitly with state.<formKey>.dispatchChanges({}, { validate: true }) before advancing - see Dispatching Changes.

Why re-seed from saved state

Without the value={data.<key>} seeding, going Back to a finished step would show empty inputs - the fields unregistered when you left and remount fresh. Capturing on every navigation and seeding from data is what makes the wizard feel stateful. (For a short form you can instead keep every step mounted and hide inactive ones with CSS, so nothing unregisters - but then every required field must be satisfied before the final native submit.)

Try it live

Loading playground…

Try advancing with an empty field - checkValidity() blocks the step and shows the error. Fill it in, go forward, then come back: your earlier answers are still there, re-seeded from saved state.

See Nested Forms for splitting a step into section components and Conditional Fields for branching within a step.