React Hook Form
A complete shared form flow with validation, review and final submission.
React Hook Form
This complete two-step example separates editable fields from saved flow data. React Hook Form validates the profile on submit; Stepperize saves it and marks the source complete in one accepted transition. The review screen can reopen the saved draft.
npm install @stepperize/react@^8.0.0 zod react-hook-form @hookform/resolversComplete example
Provide onFinish from your application to perform the final request. It should reject on failure. This example handles that error and lets the user retry. It uses Zod 4; see the resolver documentation for library-specific validation details.
"use client";
import { useState } from "react";
import { defineStepper } from "@stepperize/react";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const profileSchema = z.object({
name: z.string().min(1, "Enter your name"),
email: z.string().email("Enter a valid email"),
});
type Profile = z.infer<typeof profileSchema>;
const signup = defineStepper([
{ id: "profile", title: "Profile", schema: profileSchema },
{ id: "review", title: "Review" },
]);
type Props = { onFinish: (profile: Profile) => Promise<void> };
export function Signup({ onFinish }: Props) {
return <signup.Provider><Flow onFinish={onFinish} /></signup.Provider>;
}
function Flow({ onFinish }: Props) {
const stepper = signup.useStepperContext();
return stepper.match({
profile: () => <ProfileForm />,
review: () => <Review onFinish={onFinish} />,
});
}
function ProfileForm() {
const stepper = signup.useStepperContext();
const { register, handleSubmit, formState } = useForm<Profile>({
resolver: zodResolver(profileSchema),
defaultValues: stepper.data.get("profile") ?? { name: "", email: "" },
});
return (
<form noValidate onSubmit={handleSubmit(async (data) => {
await stepper.next({ data, complete: true });
})}>
<label>Name <input {...register("name")} aria-invalid={!!formState.errors.name} /></label>
{formState.errors.name && <p role="alert">{formState.errors.name.message}</p>}
<label>Email <input type="email" {...register("email")} aria-invalid={!!formState.errors.email} /></label>
{formState.errors.email && <p role="alert">{formState.errors.email.message}</p>}
<button type="submit" disabled={formState.isSubmitting || !stepper.canNext}>Review</button>
</form>
);
}
function Review({ onFinish }: Props) {
const stepper = signup.useStepperContext();
const profile = stepper.data.get("profile");
const [pending, setPending] = useState(false);
const [error, setError] = useState("");
const [finished, setFinished] = useState(false);
async function finish() {
if (!profile || pending) return;
setPending(true);
setError("");
try {
await onFinish(profile);
stepper.setComplete("review");
setFinished(true);
} catch {
setError("Could not save. Please try again.");
} finally {
setPending(false);
}
}
if (finished) return <p role="status">Saved successfully.</p>;
return (
<section>
<h2>Review</h2>
<p>{profile?.name} — {profile?.email}</p>
{error && <p role="alert">{error}</p>}
<button type="button" disabled={pending} onClick={() => stepper.prev()}>Edit</button>
<button type="button" disabled={!profile || pending} onClick={finish}>
{pending ? "Saving…" : "Finish"}
</button>
</section>
);
}Why this works
- One Provider owns the flow. Every child uses
useStepperContext(). - Field errors prevent the form submit callback from navigating.
next({ data, complete: true })saves the profile and completes the profile step together.- Returning to the profile remounts the form, seeded from
data.get("profile"). - The final screen calls your application's
onFinish; it does not callnext()at a boundary.
If you expose other navigation paths, protect forward transitions with beforeStepChange on the Provider as well. linear controls eligible targets; it does not validate form fields. For async guards, handle rejected results and errors in the form submit path.
For keeping unsaved component-local fields mounted, see React Activity or Content forceMount. Compare form patterns and local/shared ownership.
Last updated on