Build a custom React hook that drives a multi-step flow — a checkout wizard, an onboarding tour, a survey split across pages. useStep(maxStep) owns a single 1-indexed step number and hands back that number plus a bundle of helpers to move through the flow: go forward, go back, jump to a specific step, and reset. The interesting part isn't counting; it's keeping the step bounded — it can never run past the last step or below the first — and making the controls stack correctly when several fire in one event.
function useStep(maxStep: number): [
currentStep: number,
helpers: {
goToNextStep: () => void;
goToPrevStep: () => void;
reset: () => void;
canGoToNextStep: boolean;
canGoToPrevStep: boolean;
setStep: (step: number) => void;
},
];
currentStep starts at 1. Steps are 1-indexed and run from 1 to maxStep inclusive. The hook returns a tuple — a currentStep value and a helpers object — so a component can name them however it likes.
function Wizard() {
const [step, { goToNextStep, goToPrevStep, canGoToNextStep, canGoToPrevStep }] =
useStep(3);
return (
<div>
<p>Step {step} of 3</p>
<button onClick={goToPrevStep} disabled={!canGoToPrevStep}>Back</button>
<button onClick={goToNextStep} disabled={!canGoToNextStep}>Next</button>
</div>
);
}
// renders "Step 1 of 3"; Back is disabled. Next -> "Step 2 of 3";
// Next -> "Step 3 of 3" and Next becomes disabled.
// Steps are clamped to [1, maxStep]. Moving past an edge is a no-op.
const [step, { goToNextStep, goToPrevStep, setStep }] = useStep(3);
goToPrevStep(); // already at 1 -> stays 1
goToNextStep(); // 1 -> 2
goToNextStep(); // 2 -> 3
goToNextStep(); // already at 3 (maxStep) -> stays 3
setStep(99); // out of range -> ignored, stays 3
setStep(2); // in range -> 2
1, not 0, and the last valid step is maxStep.goToNextStep at maxStep leaves the step unchanged; goToPrevStep at 1 stays at 1. Nothing throws.setStep ignores out-of-range values. A jump to n only takes effect when 1 <= n <= maxStep; anything outside that range is silently ignored, leaving the current step in place.canGoToNextStep and canGoToPrevStep are booleans. canGoToNextStep is true when currentStep + 1 <= maxStep; canGoToPrevStep is true when currentStep - 1 >= 1. They are values, not functions — read them directly.goToNextStep calls in one handler should advance by two, not one.