react-data-form
Multi-step forms
Turning the same description into a wizard, with per-step validation.
A wizard is a grouped form walked one section at a time. The field descriptions do not change at all — only the entry point and two of the form's components.
createStepForm#
import { createStepForm } from "react-data-form/step"
const form = createStepForm({
label: { title: "New project", submit: "Create the project" },
groupOption: {
itemGroups: [
{ group: "type", title: "What are we building?", order: 10 },
{ group: "details", title: "The details", order: 20 },
{
group: "team",
title: "Who is on it?",
order: 30,
// Runs before moving on — reserve a slug, check a quota…
onNextStep: async ({ form }) => {
await api.post("/projects/precheck", form.data)
return form
},
},
],
},
inputs: {
kind: { label: "Project type", groups: ["type"], controller: SelectCardInputController, valueOptions: KINDS },
name: { label: "Name", groups: ["details"], required: true },
summary: { label: "Summary", groups: ["details"] },
lead: { label: "Project lead", groups: ["team"] },
},
})A three-step form — try submitting with no name
createStepForm builds the form, applies the lowest-order step, and swaps two components: formSubmitAction becomes the step navigation, and formDecorator the progress header.
Validation per step#
Two things happen that an ordinary form does not do:
- Moving forward validates the current step.
goToNextStepruns the validators and refuses to advance while a field is in violation — so an error never scrolls off behind the reader. - A failed submit walks back. When the final submit raises a violation on a field belonging to an earlier step, the wizard returns to the first step holding one, instead of showing a message about a field that is not on screen.
This is why the last step is not enough
Server-side violations arrive after the whole payload is sent. Without that walk-back the reader would be told the form is invalid while looking at a page where everything is filled in.
Doing work between steps#
onNextStep is awaited before the wizard advances, and can return a modified form. That is the hook for anything the next step depends on — reserving an identifier, checking a quota, fetching the options of a field further along.
onNextStep: async ({ step, form }) => {
const { data } = await api.post("/projects/precheck", form.data)
// Fill the next step's options from what the server just answered.
form.inputs.lead.valueOptions = valueOptionMapper(data.members, "name", "@id")
return form
}It exists in two places: on groupOption, where it runs between every pair of steps, and on a single step, where it runs only when leaving that one. Both run, the shared one first.
useFormStep#
The default navigation covers most wizards. When it does not, drive your own from the hook — it works anywhere inside the form's provider.
import { useFormStep } from "react-data-form/step"
function StepFooter() {
const { currentStep, steps, progress, goToNextStep, goToPrevStep, onSubmit } =
useFormStep()
return (
<footer>
<progress value={progress} max={100} />
<button onClick={goToPrevStep}>Back</button>
<button onClick={goToNextStep}>Next</button>
</footer>
)
}| Name | Type | Default | Description |
|---|---|---|---|
currentStep | FormStep | undefined | — | The step on screen. |
nextStep / previousStep | FormStep | undefined | — | Its neighbours, or undefined at either end. |
steps | FormStep[] | — | Every step, sorted by order. |
progress | number | — | 0–100, from the current step's position. |
goToNextStep | () => void | — | Validates, runs onNextStep, then advances. |
goToPrevStep | () => void | — | Goes back. No validation — going back is never blocked. |
onSubmit | () => void | — | Submits, and walks back to the first step in violation. |
form | FormWithStepsBuild | — | The form itself, should you need to read it. |
Step options#
A step is an item group with one extra key, so everything on the groups page applies — title, description, icon, order.
| Name | Type | Default | Description |
|---|---|---|---|
groupOption.itemGroupsrequired | FormStep[] | — | The steps. Empty throws rather than rendering nothing. |
groupOption.currentStep | FormStep | — | Where the wizard is. Set for you; read it if you need to. |
groupOption.hideStepNumber | boolean | — | Hides the “2 / 4” counter in the header. |
groupOption.onNextStep | ({ step, form }) => Promise<Form> | — | Runs between every pair of steps. |
step.onNextStep | ({ step, form }) => Promise<Form> | — | Runs when leaving that step in particular. |