React Form Wizard
Guides

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.

Step 1 of 3 account

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

OptionTypeDescription
stepIdsstring[]Ordered step ids. Length drives totalSteps; goToId resolves against it. Pass the ids currently visible.
startIndexnumberZero-based starting step. Clamped into range on the first render.
initialDataWizardDataSeed data for uncontrolled use. Read once, like defaultValue.
dataWizardDataSupply to control data from outside.
onDataChange(data) => voidFires whenever data changes.
onStepChange(event) => void{ prevIndex, nextIndex, stepId }. Does not fire on mount.
persist{ key, storage }Keep data across reloads. See Persistence.
syncToUrlboolean | { param }Mirror the step into the URL.

Returns

ValueTypeDescription
currentStepnumberActive zero-based index.
maxVisitedStepnumberFurthest step reached — use it to gate forward jumps.
totalStepsnumberNumber of steps currently visible.
stepIdstring | undefinedId of the active step.
isFirstStep / isLastStepbooleanEdge helpers for disabling buttons.
dataWizardDataShared wizard data.
next() / previous()() => voidMove one step; clamped at the edges.
goTo(index) / goToId(id)(x) => voidJump directly. Unknown ids are ignored.
reset()() => voidReturn to startIndex.
activateAll()() => voidMark every step visited, unlocking free navigation.
setData(data)(data) => voidReplace the data object.
updateData(patch)(patch) => voidMerge a patch, leaving other keys untouched.
clearPersisted()() => voidRemove 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:

Question 1 of 2 — the list resizes as you answer.

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.

On this page