Управление формами с помощью React Hook Form и Zod

Управление формами с помощью React Hook Form и Zod

Введение

Многошаговую форму с очень большим количеством полей ввода сложно поддерживать даже в исходном виде, а при добавлении таких функций, как временное сохранение и настройка начальных значений, она очень быстро становится сложной. Поэтому при работе с формами необходима эффективная «стратегия». В этой статье я обобщу подход к стратегическому управлению сложными формами в среде TypeScript и React с использованием react-hook-form (RHF), Zod и Jotai.

Когда сложные формы начинают разрушаться?

По мере увеличения формы начинают возникать следующие проблемы.

1. Компоненты становятся перегруженными.

— По мере увеличения количества полей ввода, логики валидации, условного отображения и обработки ошибок компонент, отвечающий за управление формой, экспоненциально разрастается.

2. Значения теряются или создаются ненужные данные.

— При наличии условных полей значения полей, не соответствующих текущим условиям, могут быть потеряны или сохранены без необходимости.

3. Структура быстро усложняется по мере увеличения количества моментов обработки.

— При поддержке временного сохранения или наличии нескольких условных полей код быстро усложняется, поскольку ему приходится обрабатывать множество вариантов в зависимости от того, когда значения сохраняются или изменяются.

Настройка стратегии управления

Вместо того чтобы решать перечисленные выше проблемы по отдельности, я хотел устранить их на основе эффективных принципов управления. В результате я сформулировал следующие стратегии.

1. Упростить общий код, минимизировав дублирование логики

2. При изменении условий сбрасывать значения ненужных условных полей.

3. Строить логику обработки ответов на основе значений условий и момента обработки.

4. Проверять введённые значения на каждом этапе, а затем ещё раз выполнять полную валидацию при сохранении итоговых данных.

Подробное рассмотрение стратегии

(1) Определить общие типы

// 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) Разделить код для каждого этапа и использовать extend() из RHF и discriminatedUnion из Zod

// 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) Объединить их в полную модель

// 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>;

На этом этапе значения при необходимости можно проверять с помощью функций refine() и superRefine() из Zod, а Jotai использовать для хранения текущего этапа и временных значений.

Пример реализации компонента

Компонент реализуется следующим образом. Если существуют временные значения, они используются при начальной настройке. Временное сохранение, а также сохранение текущего и завершённого этапов выполняются при отправке формы текущего этапа.

// 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 (
    ...
  )
};

Заключение

На примере приведённого выше краткого кода мы рассмотрели, как разработать и применить стратегию работы со сложными формами. Благодаря предварительной разработке базовых принципов и стратегий стало возможно не только эффективнее управлять формами, но и значительно сократить время разработки и сопровождения. Этот опыт показал мне, что качественный код также может быть результатом правильно выбранных правил.

Май

Site footer