useStep
EditNavigates a finite sequence of steps with optional wrapping.
import { useCallback, useEffect, useState } from "react";
import type { Dispatch, SetStateAction } from "react";
export interface UseStepOptions { readonly initialStep?: number; readonly loop?: boolean;}
export interface UseStepResult { readonly currentStep: number; readonly setStep: Dispatch<SetStateAction<number>>; readonly next: () => void; readonly previous: () => void; readonly reset: () => void; readonly isFirst: boolean; readonly isLast: boolean;}
function assertStepCount(stepCount: number): void { if (!Number.isSafeInteger(stepCount) || stepCount < 1) { throw new RangeError("stepCount must be a positive safe integer"); }}
function normalizeStep(step: number, stepCount: number, loop: boolean): number { if (!Number.isSafeInteger(step)) { throw new RangeError("step must be a safe integer"); }
if (loop) return ((step % stepCount) + stepCount) % stepCount; return Math.min(stepCount - 1, Math.max(0, step));}
export function useStep( stepCount: number, options: UseStepOptions = {},): UseStepResult { const { initialStep = 0, loop = false } = options; assertStepCount(stepCount);
const normalizedInitial = normalizeStep(initialStep, stepCount, loop); const [storedStep, setStoredStep] = useState(normalizedInitial); const currentStep = normalizeStep(storedStep, stepCount, loop);
useEffect(() => { if (storedStep !== currentStep) setStoredStep(currentStep); }, [currentStep, storedStep]);
const setStep = useCallback<Dispatch<SetStateAction<number>>>( (action) => { setStoredStep((current) => { const normalizedCurrent = normalizeStep(current, stepCount, loop); const next = typeof action === "function" ? action(normalizedCurrent) : action; return normalizeStep(next, stepCount, loop); }); }, [loop, stepCount], );
const next = useCallback(() => { setStep((step) => step + 1); }, [setStep]); const previous = useCallback(() => { setStep((step) => step - 1); }, [setStep]); const reset = useCallback(() => { setStoredStep(normalizeStep(initialStep, stepCount, loop)); }, [initialStep, loop, stepCount]);
return { currentStep, setStep, next, previous, reset, isFirst: currentStep === 0, isLast: currentStep === stepCount - 1, };}Download
Section titled “Download”wget -O src/hooks/useStep.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/react-hooks/useStep.tstemp_dir="$(mktemp -d)" && bunx degit jrTilak/lazykit/registry/react-hooks "$temp_dir" && mv "$temp_dir/useStep.ts" src/hooks/useStep.ts && rm -r "$temp_dir"Examples
Section titled “Examples”import { useStep } from "./useStep";
const steps = ["Account", "Profile", "Review"] as const;
export function StepExample() { const { currentStep, next, previous, reset, isFirst, isLast } = useStep( steps.length, );
return ( <div> <p> Step {currentStep + 1} of {steps.length}: {steps[currentStep]} </p> <button type="button" onClick={previous} disabled={isFirst}> Previous </button> <button type="button" onClick={next} disabled={isLast}> Next </button> <button type="button" onClick={reset}>Start over</button> </div> );}stepCount(number) — Positive safe integer number of zero-based steps.options.initialStep(number, default:0) — Initial and reset step.options.loop(boolean, default:false) — Wraps past either boundary instead of clamping.
Returns
Section titled “Returns”UseStepResult containing currentStep, React-compatible setStep, next,
previous, reset, isFirst, and isLast. Steps are normalized again if
stepCount changes.