Managing Forms with React Hook Form and Zod

Managing Forms with React Hook Form and Zod

Introduction

A multistep form with a very large number of input fields is difficult to maintain on its own, and becomes complex very quickly when features such as temporary saving and initial value configuration are added. Therefore, an efficient “strategy” is needed when working with forms. In this article, I will summarize how to strategically manage complex forms in a TypeScript and React environment using react-hook-form (RHF), Zod, and Jotai.

When Do Complex Forms Begin to Break Down?

As a form grows larger, the following problems begin to occur.

1. Components become bloated.

- As input fields, validation logic, conditional rendering, and error-handling logic increase, the component responsible for managing the form grows exponentially.

2. Values are lost or unnecessary data is created.

- When conditional fields are present, values for fields that do not match the current conditions may be lost or saved unnecessarily.

3. The structure becomes rapidly more complex as the number of timings increases.

- When temporary saving is supported or there are multiple conditional fields, the code becomes rapidly more complex because it must handle a wide variety of cases depending on when values are saved or changed.

Setting Up a Management Strategy

Rather than handling the above problems individually, I wanted to address them according to efficient management principles. The strategies I established are as follows.

1. Simplify the overall code by minimizing duplicated logic

2. When conditions change, reset the values of unnecessary conditional fields.

3. Structure the response logic based on condition values and timing.

4. Validate input values at each step, then perform full validation once more when saving the final data.

Elaborating on the Strategy

(1) Define common types

// schemas/common.ts
import { z } from "zod";

export const MemberType = z.enum(["personal", "employee", "freelancer", "business"]);
export type MemberType = z.infer<typeof MemberType>;

export const basicProfileSchema = z.object({
  nickname: z.string().min(2, "닉네임은 2자 이상이어야 합니다.").max(20),
  ...
});

(2) Separate the code for each step and utilize RHF's extend() and Zod's discriminatedUnion

// schemas/steps/job.schema.ts
import { z } from "zod";
import { MemberType } from "../common";

const baseJob = z.object({
  memberType: MemberType,
  ...
});

const employeeJob = baseJob.extend({
  memberType: z.literal("employee"),
  ...
});

const freelancerJob = baseJob.extend({
  memberType: z.literal("freelancer"),
  ...
});

const businessJob = baseJob.extend({
  memberType: z.literal("business"),
  ...
});

const personalJob = baseJob.extend({
  memberType: z.literal("personal"),
  ...
});

export const jobStepSchema = z.discriminatedUnion("memberType", [
  personalJob,
  employeeJob,
  freelancerJob,
  businessJob,
]);

export type JobStepForm = z.infer<typeof jobStepSchema>;

(3) Combine them into a full model

// schemas/profile.schema.ts
import { z } from "zod";
import { basicProfileSchema } from "./common";
import { jobStepSchema } from "./steps/job.schema";

export const profileSchema = z.object({
  profile: basicProfileSchema,
  job: jobStepSchema,
  ...
});

export type ProfileForm = z.infer<typeof profileSchema>;

At this point, values can be validated using Zod's refine() and superRefine() functions when necessary, while Jotai is used to store the current step and temporary values.

Component Implementation Example

The component is implemented as follows. If temporary values exist, they are used for the initial setup. Temporary saving and saving the current/completed step are performed when the form for the current step is submitted.

// steps/Job.tsx
import React, { useEffect } from "react";
import { useAtom } from "jotai";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

import { draftAtom, completedStepsAtom, currentStepAtom } from "../state/profileWizard.atoms";
import { mergeDraft } from "../state/mergeDraft";
import { jobStepSchema, type JobStepForm } from "../schemas/steps/Job.schema";

export function Job() {
  const [draft, setDraft] = useAtom(draftAtom); // 임시 저장값 atom
  const [completedSteps, setCompletedSteps] = useAtom(completedStepsAtom); // 완료된 단계를 저장하는 atom
  const [currentStep, setCurrentStep] = useAtom(currentStepAtom); // 현재 단계를 나타내는 atom

  const form = useForm<JobStepForm>({
    resolver: zodResolver(jobStepSchema),
    defaultValues: {
      ...
    },
  });
  const memberType = form.watch("memberType");

  const handleSubmit = (values) => {
    // 1) 현재 스텝 값을 draft에 저장
    setDraft(mergeDraft(draft, { job: values }));

    // 2) 스텝 완료 처리
    const nextCompleted = new Set(completedSteps);
    nextCompleted.add(4); setCompletedSteps(nextCompleted);

    // 3) 다음 스텝 이동
    setCurrentStep((currentStep + 1) as any);
  }

  // 스텝 진입 시 임시 저장값으로 초기화
  useEffect(() => {
    if (!!draft?.job) {
      form.reset(draft.job);
    }
  }, []);

  // 조건 변경 시 불필요한 조건부 필드의 값 초기화 (관리 전략 2)
  useEffect(() => {
    const base = form.getValues();

    if (memberType === "employee") {
      form.reset({
        memberType,
        role: base.role,
        ...
      } as JobStepForm);
    }

    if (memberType === "freelancer") {
      form.reset({
        memberType,
        role: base.role,
        ...
      } as JobStepForm);
    }

    if (memberType === "business") {
      form.reset({
        memberType,
        role: base.role,
        ...
      } as JobStepForm);
    }

    if (memberType === "personal") {
      form.reset({
        memberType,
        role: base.role,
        ...
      } as JobStepForm);
    }
  }, [memberType]);

  return (
    ...
  )
};

Conclusion

Through the brief example code above, we explored how to establish and apply a strategy for handling complex forms. By setting basic principles and strategies first, it became possible not only to control forms more effectively but also to significantly reduce development and maintenance time. This experience taught me that high-quality code can also emerge from appropriate policies.

May

Site footer