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:
- 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. - Preserve values across steps. A
GInputunregisters 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 itsvalueprop.
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);
}}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.
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
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.