Stepper instance
Reference the runtime API returned by the hook.
Stepper instance
useStepper() and useStepperContext() return the same flat snapshot shape. The first owns local state; the second consumes a shared owner.
Example setup
All examples on this page use this stepper definition:
import { defineStepper } from "@stepperize/react";
const checkout = defineStepper([
{ id: "shipping", title: "Shipping" },
{ id: "payment", title: "Payment" },
{ id: "review", title: "Review" },
]);This page is the checklist for what you can read or call after creating an instance:
const stepper = checkout.useStepper();State
| Property | Description |
|---|---|
steps | All step data in order. |
current / id | Current step object and id. |
index / count | Zero-based index and total count. |
progress | Number from 0 to 1. |
completed | Explicitly completed step ids. |
isFirst / isLast | Edge state. |
canPrev / canNext | Whether prev or next can currently move. |
isPending | Whether an async change is running. |
const label = `${stepper.index + 1} of ${stepper.count}`;
const percent = Math.round(stepper.progress * 100);Step access
The runtime instance only carries live state and actions. Static step lookups
live on the definition and on the raw stepper.steps array:
checkout.get("payment"); // step by id (definition helper)
checkout.at(0); // step by index (definition helper)
checkout.parseStep(urlParam); // narrow an unknown value to a step id
const index = stepper.index;Status and rendering
stepper.status("shipping"); // "active" | "previous" | "upcoming"
stepper.is("review"); // booleanreturn stepper.match({
shipping: () => <Shipping />,
payment: () => <Payment />,
review: () => <Review />,
});Flow data
Flow data is committed, cross-step data — not live field state. With a per-step
schema, data.get(id) is typed as that schema's input.
| Method | Description |
|---|---|
data.get() | Current step's data (unknown). |
data.get(id) | A specific step's data, typed from its schema. |
data.set(value) | Set the current step's data. |
data.set(id, value) | Set a specific step's data. |
data.update(id, updater) | Compose a typed update against the latest data, including earlier writes in the same event. |
data.all() | All flow data, keyed by step id. |
data.clear(id?) | Clear one step (or all when no id). |
data.reset() | Reset to defaults. |
stepper.data.set("shipping", { name: "Ada" });
const all = stepper.data.all(); // for review / summary screensValidation
validate checks a step's stored data against its schema and resolves to a
discriminated result. Steps without a schema always succeed.
const result = await stepper.validate("shipping"); // defaults to the current step
if (result.success) {
// result.data is the schema OUTPUT type
} else {
// result.issues: readonly { message; path? }[]
}Completion
stepper.setComplete(); // mark current step complete
stepper.setComplete("shipping"); // mark a specific step complete
stepper.setComplete("shipping", false); // mark it incomplete
stepper.isComplete("shipping");Completion is separate from positional status.
Navigation
stepper.next(payload);
stepper.prev(payload);
stepper.goTo("review", payload);
stepper.reset(options);
stepper.canGoTo("review");Navigation can be called directly from buttons. Await it when you need the result.
const accepted = await stepper.next(payload);Navigation resolves to { accepted: true, from, to } or { accepted: false, reason }. Check result.accepted, never the object's truthiness.
next, prev and goTo accept an optional { data, complete } payload, staged for the
current step before the guard runs and committed only if the move is
accepted:
const accepted = await stepper.next({ data: currentStepData });To write several steps at once, call stepper.data.set first, then navigate.
Lifecycle
The instance does not expose imperative event subscriptions. Register a guard and an after-effect through the hook options instead:
const stepper = checkout.useStepper({
beforeStepChange: async ({ direction, validate }) => {
if (direction === "prev") return true;
return (await validate()).success; // false cancels
},
onStepChange: (step) => analytics.track("step_view", { step }),
});For ad-hoc effects on any committed change, use an effect on stepper.id:
React.useEffect(() => analytics.track("step_view", { step: stepper.id }), [stepper.id]);Change context
beforeStepChange and onStepChange receive the same context shape.
type StepChangeContext = {
from: Step;
to: Step;
fromIndex: number;
toIndex: number;
direction: "next" | "prev" | "goto" | "reset";
data: FlowData;
signal: AbortSignal;
validate: (idOrStep?: StepId | Step) => Promise<ValidationResult>;
statuses: Record<StepId, "active" | "previous" | "upcoming">;
};In beforeStepChange, data already includes the pending navigation payload and
validate() validates the step being left against that same data snapshot. In
onStepChange, statuses reflect the accepted destination.
Atomic completion and reset
next({ data: values, complete: true }) commits source data and source completion together after guard acceptance. goTo(id, { bypassPolicy: true }) bypasses linear eligibility but still runs the guard.
reset() restores mount-time step, data and completion. Pass { keepData: true, keepCompleted: true } to preserve business state. data.clear() empties flow data; data.reset() restores default data only. See navigation.
match handlers receive (step, data), with data inferred from that step's schema input (or unknown without a schema). The handler map remains exhaustive.
Last updated on