Stepperize v8Explore the changes

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

PropertyDescription
stepsAll step data in order.
current / idCurrent step object and id.
index / countZero-based index and total count.
progressNumber from 0 to 1.
completedExplicitly completed step ids.
isFirst / isLastEdge state.
canPrev / canNextWhether prev or next can currently move.
isPendingWhether 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"); // boolean
return 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.

MethodDescription
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 screens

Validation

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.

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.

Edit on GitHub

Last updated on

On this page