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.

Building Multi-Step Forms in React with TypeScript, React-Hook-Form, and Zod

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