How to Build a Multi-Step Form with React Hook Form and Zod
Why Multi-Step Forms Are Hard
Multi-step forms fail in predictable ways: state lost when navigating back, validation running on the wrong step, confusing UX when errors appear, and form data lost on page refresh. Getting them right requires deliberate architecture choices before writing any JSX.
The State Management Approach
Use a single React Hook Form instance at the top level that holds all form data across all steps. Do not reset the form when moving between steps — persist the data. Use useForm with a Zod schema that covers the entire form, and validate only the current step's fields on each "Next" click.
const schema = z.object({
// Step 1
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.string().email(),
// Step 2
company: z.string().min(1),
role: z.string().min(1),
// Step 3
plan: z.enum(['starter', 'pro', 'enterprise']),
billingCycle: z.enum(['monthly', 'annual']),
});
const form = useForm>({
resolver: zodResolver(schema),
defaultValues: { firstName: '', lastName: '', email: '', company: '', role: '', plan: 'pro', billingCycle: 'monthly' },
});
Step-Level Validation
On each "Next" click, trigger validation for only the current step's fields using form.trigger(stepFields). If validation passes, advance the step. If it fails, React Hook Form automatically focuses the first error field.
const stepFields = {
0: ['firstName', 'lastName', 'email'] as const,
1: ['company', 'role'] as const,
2: ['plan', 'billingCycle'] as const,
};
const handleNext = async () => {
const valid = await form.trigger(stepFields[currentStep]);
if (valid) setCurrentStep(prev => prev + 1);
};
Persisting State on Refresh
Use sessionStorage to persist form data across page refreshes. Watch the form values with form.watch() and write to sessionStorage on every change. On mount, read from sessionStorage and call form.reset(savedData) to restore. Clear sessionStorage on successful submission.
Progress Indication
Show a progress bar or step indicator. Users abandon multi-step forms when they cannot see how much is left. Calculate progress as (currentStep / totalSteps) * 100 and animate the progress bar with a CSS transition. Label each step so users understand what is coming.
I use this pattern in every SaaS onboarding flow I build. See production examples or reach out to discuss your project.
Hire me for similar projects
Looking for a developer who can build what you just read about? Let's talk.
Get in Touch