Building Multi-Step Forms in React with TypeScript, React-Hook-Form, and Zod
Leveraging React, TypeScript, React-Hook-Form, and Zod streamlines multi-step form creation, enhancing UX and simplifying validation. This approach offers a robust solution for efficient, error-free form management in web applications.
In this blog post, we'll walk through an example of creating a multi-step form in React using TypeScript, the react-hook-form
library, and zod
for schema validation. This setup is particularly useful for larger forms where you want to break down the input process into more manageable chunks, improving the user experience. Our example involves a two-step form for collecting basic contact information: the first step collects the user's first and last name, and the second step collects their email and phone number.
Setting Up Our Environment
First, ensure you have a React environment setup with TypeScript. We'll also need to install react-hook-form
and zod
as dependencies:
npm install react-hook-form zod @hookform/resolvers
Defining Our Schemas with Zod
zod
allows us to define schemas for our form data, ensuring that the data matches our expectations before we attempt to use it. In this example, we define two separate schemas for each step of our form process and then combine them into a discriminated union. This approach allows us to validate the form data against the correct schema based on the current step of the form.
import { z } from "zod";
const stepOneSchema = z.object({
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"),
});
const stepTwoSchema = z.object({
email: z.string().email("Invalid email address"),
phoneNumber: z.string().min(10, "Invalid phone number"),
});
const formSchema = z.discriminatedUnion("step", [
z.object({ step: z.literal(1) }).extend(stepOneSchema.shape),
z.object({ step: z.literal(2) }).extend(stepTwoSchema.shape),
]);
Creating Our Form Components
We use react-hook-form
to manage our form state and submission. The FormProvider
component from react-hook-form
allows us to define our form at a high level and then access the form's context from our step components.
Each step of the form is defined as a separate component (StepOne
and StepTwo
), which uses the useFormContext
hook to access the form's register function. This function is used to register input fields in the form.
import { useForm, FormProvider, useFormContext } from "react-hook-form";
const StepOne = () => {
const { register } = useFormContext();
return (
<>
<input {...register("firstName")} placeholder="First Name" />
<input {...register("lastName")} placeholder="Last Name" />
</>
);
};
const StepTwo = () => {
const { register } = useFormContext();
return (
<>
<input {...register("email")} placeholder="Email" />
<input {...register("phoneNumber")} placeholder="Phone Number" />
</>
);
};
Managing Form Submission and State
Our main App
component manages the current step of the form and handles form submission. We use the useState
hook to track the current form step and the useForm
hook to initialize our form with react-hook-form
. The resolver
option is used with zodResolver
to integrate our zod
validation schema with react-hook-form
.
Upon form submission, if we're not on the last step, we simply increment the current step. Otherwise, we handle the final form submission (e.g., sending the data to an API).
import { useState } from "react";
export default function App() {
const [currentStep, setCurrentStep] = useState(1);
const methods = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
step: currentStep,
},
});
const onSubmit = (data) => {
if (currentStep < 2) {
setCurrentStep(currentStep + 1);
} else {
console.log("Form submitted");
console.log(data);
// Handle final form submission here
}
};
return (
<div className="App">
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
{currentStep === 1 && <StepOne />}
{currentStep === 2 && <StepTwo />}
<button type="submit">{currentStep < 2 ? "Next" : "Submit"}</button>
</form>
</FormProvider>
</div>
);
}
Conclusion
This example demonstrates a powerful pattern for managing complex form workflows in React applications using TypeScript. By leveraging react-hook-form
for form state management and zod
for schema validation, we can create robust, maintainable forms that improve the user experience by breaking down the input process into simpler, more manageable steps. Absolutely, let's complete the thought from where it was left off:
This pattern can be extended to accommodate forms with any number of steps, making it a versatile solution for various use cases. By organizing our form into discrete steps, we not only make the form easier for the user to understand and complete, but we also simplify the validation logic by isolating it to specific segments of the form data.
Furthermore, integrating TypeScript provides additional benefits by ensuring type safety across our form components and validation logic. This helps catch errors early in the development process, making our code more robust and maintainable.
React-Hook-Form's performance optimization and minimal re-rendering philosophy ensure that our multi-step form remains efficient, even as complexity grows. The use of Zod for schema validation introduces a layer of reliability and developer-friendly error handling that improves the overall quality of user input.
In conclusion, the combination of React with TypeScript, React-Hook-Form, and Zod offers a powerful stack for building and managing forms in web applications. This setup not only enhances developer productivity through type safety and efficient state management but also elevates the user experience by providing clear, step-by-step guidance through complex input processes. Whether you're collecting user information, conducting surveys, or creating multi-part registrations, this approach to building multi-step forms can help streamline your development process and produce high-quality, user-friendly forms.
Full Source Code : https://codesandbox.io/p/devbox/z833k3?file=%2Fsrc%2FApp.tsx