npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gooonzick/wizard-react

v1.12.0

Published

React integration for Wizard framework

Readme

@gooonzick/wizard-react

React integration for the WizardForm state machine. Provides a convenient useWizard() hook that connects your wizard definitions to React state.

Features

  • React Hook - Simple useWizard() hook for React components
  • Organized API - State grouped into logical slices (state, validation, navigation, loading, actions)
  • Navigation History - canGoBack and stepHistory for history-based back navigation
  • Granular Hooks - Fine-grained subscriptions with useWizardData(), useWizardNavigation(), etc.
  • Optional Provider - WizardProvider for performance optimization in complex UIs
  • Full Type Safety - TypeScript generics for your data types
  • State Management - Built-in React state handling with zero configuration
  • Loading States - Track validation, submission, and navigation
  • Backwards Compatible - 100% compatible with existing code

Installation

npm install @gooonzick/wizard-core @gooonzick/wizard-react

Quick Start

import { useWizard } from "@gooonzick/wizard-react";
import { createLinearWizard } from "@gooonzick/wizard-core";

type SignupData = {
  name: string;
  email: string;
};

const wizard = createLinearWizard<SignupData>({
  id: "signup",
  steps: [
    {
      id: "name",
      title: "What's your name?",
      validate: (data) => ({
        valid: Boolean(data.name),
        errors: data.name ? undefined : { name: "Required" },
      }),
    },
    {
      id: "email",
      title: "What's your email?",
      validate: (data) => ({
        valid: data.email?.includes("@") ?? false,
        errors: data.email?.includes("@")
          ? undefined
          : { email: "Invalid email" },
      }),
    },
  ],
});

export function SignupForm() {
  const { state, navigation, actions, validation, loading } = useWizard({
    definition: wizard,
    initialData: { name: "", email: "" },
  });

  return (
    <div>
      <h2>{state.currentStep.meta?.title}</h2>

      {state.currentStepId === "name" && (
        <input
          value={state.data.name}
          onChange={(e) => actions.updateField("name", e.target.value)}
          placeholder="Name"
        />
      )}

      {state.currentStepId === "email" && (
        <input
          value={state.data.email}
          onChange={(e) => actions.updateField("email", e.target.value)}
          placeholder="Email"
        />
      )}

      {validation.validationErrors && (
        <ul className="errors">
          {Object.entries(validation.validationErrors).map(([field, error]) => (
            <li key={field}>{error}</li>
          ))}
        </ul>
      )}

      <button
        onClick={() => navigation.goPrevious()}
        disabled={!navigation.canGoPrevious || loading.isNavigating}
      >
        Previous
      </button>
      <button
        onClick={() => navigation.goNext()}
        disabled={loading.isNavigating}
      >
        {/* On the last step goNext() completes the wizard; canGoNext is false there. */}
        {navigation.isLastStep ? "Complete" : "Next"}
      </button>
    </div>
  );
}

Core Concepts

useWizard Hook

The useWizard() hook connects a wizard definition to React state and returns an organized object with state grouped into logical slices:

const { state, validation, navigation, loading, actions } = useWizard({
  definition: wizardDefinition,
  initialData: myInitialData,
  context: myContext,
  onComplete: (data) => {
    // Handle completion
  },
});

Organized State Structure

The hook returns state grouped into five logical slices for clarity:

// State slice - current step and data
state.currentStepId;
state.currentStep;
state.data;
state.isCompleted;

// Validation slice
validation.isValid;
validation.validationErrors;

// Navigation slice (state + methods)
navigation.canGoNext;
navigation.canGoPrevious;
navigation.canGoBack;
navigation.isFirstStep;
navigation.isLastStep;
navigation.visitedSteps;
navigation.availableSteps;
navigation.stepHistory;
navigation.goNext();
navigation.goPrevious();
navigation.goBack(n); // deprecated
navigation.goTo(stepId); // validates current step first
navigation.goTo(stepId, options); // with GoToOptions
navigation.goToStep(stepId); // @deprecated — use goTo() instead

// Loading slice
loading.isValidating;
loading.isSubmitting;
loading.isNavigating;
loading.isLoadingStep; // a lazy step's implementation is loading

// Actions slice
actions.updateField("name", "John");
actions.updateData((d) => ({ ...d, name: "John" }));
actions.setData(newData);
actions.validate();
actions.validateAll(); // dry-run all enabled steps → ValidationSummary
actions.canSubmit();
actions.submit();
actions.reset();
actions.cancel(); // onCancel + reset
actions.serialize(); // / actions.restore(saved)
actions.preloadStep("documents"); // prefetch a lazy step (never rejects)

Plugins

Pass a reference-stable plugins array (hoist or useMemo) — read once at machine creation:

const plugins = useMemo(() => [createLoggingPlugin({ level: "info" })], []);

useWizard({ definition, initialData, plugins });

Hook API

useWizard Options

interface UseWizardOptions<T> {
  definition: WizardDefinition<T>;
  initialData: T;
  context?: WizardContext;
  onStateChange?: (state: WizardState<T>) => void;
  onStepEnter?: (stepId: string, data: T) => void;
  onStepLeave?: (stepId: string, data: T) => void;
  onComplete?: (data: T) => void;
  onCancel?: (data: T) => void | Promise<void>;
  onReset?: () => void;
  onError?: (error: Error) => void;
  plugins?: WizardPlugin<T>[];
}

useWizard Return Value

interface UseWizardReturn<T> {
  state: UseWizardState<T>;
  validation: UseWizardValidation;
  navigation: UseWizardNavigation;
  loading: UseWizardLoading;
  actions: UseWizardActions<T>;
}

interface UseWizardState<T> {
  currentStepId: string;
  currentStep: WizardStepDefinition<T>;
  data: T;
  isCompleted: boolean;
  stepStatuses: Record<string, StepStatus>;
  progress: WizardProgress;
}

interface UseWizardValidation {
  isValid: boolean;
  validationErrors?: Record<string, string>;
}

interface UseWizardNavigation {
  canGoNext: boolean;
  canGoPrevious: boolean;
  canGoBack: boolean;
  isFirstStep: boolean;
  isLastStep: boolean;
  visitedSteps: string[];
  availableSteps: string[];
  stepHistory: string[];
  goNext(): Promise<void>;
  goPrevious(): Promise<void>;
  /** @deprecated Use goPrevious() instead */
  goBack(steps?: number): Promise<void>;
  goTo(stepId: string, options?: GoToOptions): Promise<void>;
  /** @deprecated Use goTo(stepId) instead */
  goToStep(stepId: string): Promise<void>;
}

interface UseWizardLoading {
  isValidating: boolean;
  isSubmitting: boolean;
  isNavigating: boolean;
  isLoadingStep: boolean; // a lazy step's implementation is loading
}

interface UseWizardActions<T> {
  updateData(updater: (data: T) => T): void;
  setData(data: T): void;
  updateField<K extends keyof T>(field: K, value: T[K]): void;
  validate(): Promise<void>;
  validateAll(options?: { updateStatuses?: boolean }): Promise<ValidationSummary>;
  canSubmit(): Promise<boolean>;
  submit(): Promise<void>;
  reset(data?: T): void;
  cancel(): Promise<void>;
  serialize(): WizardSerializedState<T>;
  restore(state: WizardSerializedState<T>): void;
  preloadStep(stepId: string): Promise<void>; // prefetch a lazy step; never rejects
}

Granular Hooks (Optional Provider)

For performance-critical applications, use WizardProvider with granular hooks to subscribe only to relevant state changes:

import {
  WizardProvider,
  useWizardData,
  useWizardNavigation,
  useWizardLoading,
  useWizardActions,
} from "@gooonzick/wizard-react";

function App() {
  return (
    <WizardProvider definition={wizard} initialData={initialData}>
      <MyWizardForm />
    </WizardProvider>
  );
}

function MyWizardForm() {
  // Only re-renders when data changes
  const { data, currentStepId } = useWizardData();

  // Only re-renders when navigation state changes
  const { isLastStep, goNext } = useWizardNavigation();

  // Only re-renders when loading flags change
  const { isNavigating } = useWizardLoading();

  // Always stable - doesn't cause re-renders
  const { updateField } = useWizardActions();

  return (
    <div>
      <input
        value={data.name}
        onChange={(e) => updateField("name", e.target.value)}
      />
      {/* On the last step goNext() completes the wizard; canGoNext is false there. */}
      <button onClick={() => goNext()} disabled={isNavigating}>
        {isLastStep ? "Finish" : "Next"}
      </button>
    </div>
  );
}

Available Granular Hooks

  • useWizardData<T>() - Current step ID, step definition, data, completion status
  • useWizardNavigation() - Navigation state and methods (canGoNext, goNext, etc.)
  • useWizardValidation() - Validation status and field errors
  • useWizardLoading() - Loading flags (isValidating, isSubmitting, isNavigating, isLoadingStep)
  • useWizardActions<T>() - All wizard action methods (updateField, submit, reset, preloadStep, etc.)

When to Use Granular Hooks

Use granular hooks when:

  • Your wizard has many fields and complex components
  • You want to prevent unnecessary re-renders
  • You're optimizing a large or performance-critical form
  • You want fine-grained control over which state changes trigger re-renders

Note: Granular hooks require WizardProvider. For simple wizards, useWizard() alone is sufficient.

Common Patterns

Conditional Step Rendering

function MyWizard() {
  const { state, navigation, actions, loading } = useWizard({
    definition,
    initialData,
  });

  return (
    <>
      {state.currentStepId === "personal" && (
        <PersonalStep data={state.data} onUpdate={actions.updateField} />
      )}

      {state.currentStepId === "contact" && (
        <ContactStep data={state.data} onUpdate={actions.updateField} />
      )}

      {state.currentStepId === "review" && <ReviewStep data={state.data} />}

      <button
        onClick={() => navigation.goPrevious()}
        disabled={!navigation.canGoPrevious || loading.isNavigating}
      >
        Back
      </button>
      <button
        onClick={() => navigation.goNext()}
        disabled={loading.isNavigating}
      >
        {/* On the last step goNext() completes the wizard; canGoNext is false there. */}
        {navigation.isLastStep ? "Submit" : "Next"}
      </button>
    </>
  );
}

With React Hook Form

import { useForm } from "react-hook-form";
import { useWizard } from "@gooonzick/wizard-react";

function FormWizard() {
  const { register, watch, handleSubmit } = useForm({
    defaultValues: initialData,
  });

  const { state, navigation, actions, validation } = useWizard({
    definition,
    initialData,
  });
  const formData = watch();

  const handleNext = async () => {
    await actions.validate();
    if (validation.isValid) {
      actions.setData(formData);
      await navigation.goNext();
    }
  };

  return (
    <form onSubmit={handleSubmit(handleNext)}>
      {state.currentStepId === "personal" && (
        <>
          <input {...register("name")} />
          <input {...register("email")} />
        </>
      )}

      <button type="submit">Next</button>
    </form>
  );
}

With Context (API Client)

interface AppContext extends WizardContext {
  api: ApiClient;
}

const appContext: AppContext = {
  api: new ApiClient(),
};

function Wizard() {
  const { state, navigation, actions } = useWizard({
    definition,
    initialData,
    context: appContext,
  });

  // Definition can now use appContext in validators and hooks
}

Persisting Progress

function PersistentWizard() {
  const [savedData, setSavedData] = useState(() => {
    const saved = localStorage.getItem("wizard-data");
    return saved ? JSON.parse(saved) : initialData;
  });

  const wizard = useWizard({
    definition,
    initialData: savedData,
    onStateChange: (state) => {
      localStorage.setItem("wizard-data", JSON.stringify(state.data));
    },
  });

  return (
    <MyWizardForm
      state={wizard.state}
      actions={wizard.actions}
      navigation={wizard.navigation}
    />
  );
}

Best Practices

1. Define Wizard Outside Component

// ✅ Good - defined once
const wizard = createWizard(/* ... */).build();

function MyComponent() {
  const { state, actions, navigation } = useWizard({
    definition: wizard,
    initialData,
  });
}

// ❌ Bad - recreated every render
function MyComponent() {
  const wizard = createWizard(/* ... */).build();
  const { state, actions, navigation } = useWizard({
    definition: wizard,
    initialData,
  });
}

2. Use Separate Sub-components

// ✅ Good - concerns separated
function WizardContainer() {
  const wizard = useWizard({ definition, initialData });
  return (
    <>
      <StepRenderer state={wizard.state} actions={wizard.actions} />
      <Navigation navigation={wizard.navigation} loading={wizard.loading} />
    </>
  );
}

// ❌ Bad - everything in one component
function WizardContainer() {
  const wizard = useWizard({ definition, initialData });
  return <div>{/* all UI here */}</div>;
}

3. Await Async Operations

// ✅ Good - properly awaiting
const handleNext = async () => {
  await actions.validate();
  if (validation.isValid) {
    await navigation.goNext();
  }
};

// ❌ Bad - not awaiting
const handleNext = () => {
  actions.validate();
  navigation.goNext();
};

4. Handle Loading States

// ✅ Good - disable while loading (not on !canGoNext: goNext() finishes the last step)
<button
  onClick={() => navigation.goNext()}
  disabled={loading.isNavigating}
>
  {loading.isNavigating
    ? "Loading..."
    : navigation.isLastStep
      ? "Finish"
      : "Next"}
</button>

// ❌ Bad - no feedback to user
<button onClick={() => navigation.goNext()}>Next</button>

5. Use TypeScript

// ✅ Good - full type safety
type MyData = { name: string; email: string };
const { state, actions, navigation } = useWizard<MyData>({
  definition,
  initialData: { name: "", email: "" },
});

// ❌ Bad - no type safety
const { state, actions, navigation } = useWizard({ definition, initialData });

Documentation

For more details, see:

Peer Dependencies

  • React 19.0+
  • React-DOM 19.0+

License

MIT