Headless (useWizard)
The same wizard state machine with none of the markup.
Headless — useWizard()
v1.2
useWizard() gives you the wizard state machine with no markup, no class names
and no stylesheet. <FormWizard /> is built on the same hooks, so the two APIs
behave identically — you can move between them without relearning anything.
import { useWizard } from "react-form-wizard-component";
const STEPS = ["account", "profile", "review"];
function MyWizard() {
const wizard = useWizard({ stepIds: STEPS });
return (
<div>
<p>
Step {wizard.currentStep + 1} of {wizard.totalSteps}
</p>
{wizard.stepId === "account" && (
<input
value={String(wizard.data.email ?? "")}
onChange={(e) => wizard.updateData({ email: e.target.value })}
/>
)}
<button onClick={wizard.previous} disabled={wizard.isFirstStep}>
Back
</button>
<button onClick={wizard.next} disabled={wizard.isLastStep}>
Next
</button>
</div>
);
}Options
| Option | Type | Description |
|---|---|---|
stepIds | string[] | Ordered step ids. Length drives totalSteps; goToId resolves against it. Pass the ids currently visible. |
startIndex | number | Zero-based starting step. Clamped into range on the first render. |
initialData | WizardData | Seed data for uncontrolled use. Read once, like defaultValue. |
data | WizardData | Supply to control data from outside. |
onDataChange | (data) => void | Fires whenever data changes. |
onStepChange | (event) => void | { prevIndex, nextIndex, stepId }. Does not fire on mount. |
persist | { key, storage } | Keep data across reloads. See Persistence. |
syncToUrl | boolean | { param } | Mirror the step into the URL. |
Returns
| Value | Type | Description |
|---|---|---|
currentStep | number | Active zero-based index. |
maxVisitedStep | number | Furthest step reached — use it to gate forward jumps. |
totalSteps | number | Number of steps currently visible. |
stepId | string | undefined | Id of the active step. |
isFirstStep / isLastStep | boolean | Edge helpers for disabling buttons. |
data | WizardData | Shared wizard data. |
next() / previous() | () => void | Move one step; clamped at the edges. |
goTo(index) / goToId(id) | (x) => void | Jump directly. Unknown ids are ignored. |
reset() | () => void | Return to startIndex. |
activateAll() | () => void | Mark every step visited, unlocking free navigation. |
setData(data) | (data) => void | Replace the data object. |
updateData(patch) | (patch) => void | Merge a patch, leaving other keys untouched. |
clearPersisted() | () => void | Remove the stored payload for persist.key. |
Split hooks
useWizard is a composition of two smaller hooks, exported separately for when
the step list itself is data:
useWizardData(options)→{ data, setData, updateData, clearPersisted }useWizardCursor(options)→ the cursor and every navigation method
Reading data first and sizing the cursor from it is what lets branches appear and disappear as answers change:
Do you use React at work?
{}import { useWizardCursor, useWizardData } from "react-form-wizard-component";
function Survey() {
// Answers first: the visible question list is derived from them.
const answers = useWizardData({});
const visible = React.useMemo(
() => QUESTIONS.filter((q) => !q.showIf || q.showIf(answers.data)),
[answers.data]
);
// Then the cursor, sized by the questions that currently apply.
const cursor = useWizardCursor({ stepIds: visible.map((q) => q.id) });
const question = visible[cursor.currentStep];
// …render `question` yourself
}Reach for <FormWizard /> when you want the bundled look, and useWizard()
when the markup is yours. If you only want to restyle the bundled markup, you
do not need the headless API — see Theming and
unstyled mode.
SSR safety
Storage and URL access are guarded, so the hooks are safe to render on the server. Persistence is best-effort: private-browsing and quota errors are swallowed rather than thrown.