@avenra/react-step-form
v0.3.2
Published
Flexible multi-step forms with schema-driven validation
Maintainers
Readme
✨ react-step-form
🚀 Flexible, type-safe multi-step forms for React with schema-driven validation and fully customizable UI.
📦 Installation
npm install @avenra/react-step-form zod⚡ Quick Example
import { Controller, FormWizard, useFormWizard } from "@avenra/react-step-form";
import * as z from "zod";
const schema = z.object({
email: z.string().email(),
password: z.string().min(6),
firstName: z.string().min(1),
lastName: z.string().min(1),
});
type Values = z.infer<typeof schema>;
const TypedController = Controller<Values>;
function AccountStep() {
return (
<>
<TypedController
name="email"
render={({ field }) => <input {...field} placeholder="Email" />}
/>
<TypedController
name="password"
render={({ field }) => (
<input type="password" {...field} placeholder="Password" />
)}
/>
</>
);
}
function ProfileStep() {
const wizard = useFormWizard<Values>();
return (
<>
<TypedController
name="firstName"
render={({ field }) => (
<input {...field} placeholder="First Name" />
)}
/>
<TypedController
name="lastName"
render={({ field }) => (
<input {...field} placeholder="Last Name" />
)}
/>
<button type="button" onClick={wizard.prev}>
Previous
</button>
</>
);
}
export function RegistrationWizard() {
return (
<FormWizard
steps={[
{
id: "account",
component: AccountStep,
},
{
id: "profile",
component: ProfileStep,
},
]}
schema={schema}
persist="localStorage"
persistKey="signup-form"
debug
debugPosition="bottom-right"
onSubmit={(values) => {
console.log(values);
}}
/>
);
}🧠 Type Safety Notes
FormWizardautomatically infersvaluesfrom yourschemain most cases.Controllerprovides path-safenameand strongly typedfield.valuewhen used like this:
const TypedController = Controller<Values>;field.onChangesupports both direct values and event-like objects.- Works seamlessly with
field.onChange("text") - Also works with
<input {...field} />
- Works seamlessly with
steps[].fieldsis optional and ensures type safety for valid form paths.If
fieldsis not provided, the wizard automatically infers step fields from mountedControllernames within that step.
🔗 Nested Fields — Out of the Box
Nested paths are fully supported using a single typed controller alias.
const TypedController = Controller<Values>;
<TypedController
name="account.email"
render={({ field }) => (
<input
value={field.value ?? ""}
onChange={(e) => field.onChange(e.target.value)}
/>
)}
/>;✨ field.value is automatically inferred based on the provided name, including nested paths.
🆕 New in This Version
🔍 Automatic step field inference from mounted
Controllernames📊 Enhanced form state helpers:
isStepValiddirtyFieldstouchedFieldswatch
🧭 Derived navigation state:
totalStepscanGoNextcanGoPrevprogress
💾 Persistence hydration before first render (prevents overwriting on refresh)
⚡ Persistence writes only when values actually change
🛠️ Built-in debug panel (
debug,debugPosition) for real-time state inspection
<FormWizard
schema={schema}
steps={steps}
onSubmit={handleSubmit}
debug
debugPosition="inline"
/>🧩 API
FormWizard→ Handles context, step validation, and navigationController→ Connects any input to the wizard stateuseFormWizard→ Hook for accessing state and controlling navigation
🪝 useFormWizard Extras
isStepValid:booleandirtyFields:Record<string, boolean>touchedFields:Record<string, boolean>watch(name?): Reactively read a specific field or all valuestotalSteps,canGoNext,canGoPrev,progress
🎛️ FormWizard Render API Extras
When using children, the render API also includes:
isStepValiddirtyFieldstouchedFieldswatchtotalStepscanGoNextcanGoPrevprogress
🎯 Controller Field Extras
render({ field }) provides:
field.valuefield.onChange(next)field.onBlur()field.name
📌 For repository documentation, contribution guidelines, and release workflow, refer to the project root.
