はじめに
入力項目が非常に多い多段階フォームは、それ自体が保守しにくいだけでなく、下書き保存や初期値の設定などの機能が組み合わさると、非常に速いスピードで複雑化します。そのため、formを扱うには効率的な「戦略」が必要です。本稿では、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) 段階ごとにコードを分離し、RHFのextend()とZodの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) fullモデルとして組み合わせる
// 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>;
このとき、必要に応じてZodのrefine()、superRefine()関数を使用して値を検証し、Jotaiで現在の段階と下書き保存値を保存します。
コンポーネントの実装例
コンポーネントは次のように実装します。下書き保存値がある場合は初期設定を行い、下書き保存および現在の段階・完了段階の保存は、現在の段階のformがsubmitされた場合に実行します。
// 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 (
...
)
};
おわりに
以上のように、簡単なサンプルコードを通して、複雑なフォームを扱うための戦略の設定と適用について説明しました。このように基本原則と戦略を先に設定することで、フォームをより効果的に制御できるだけでなく、開発および保守にかかる時間も大幅に短縮できました。適切な方針によっても、品質の高いコードを生み出せることを実感した経験でした。
5月