0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React Hook Formをある程度網羅的にやってみる, part3

Posted at

この記事を書いた理由

前回の記事(Part 2)では、無名ナンバリングシリーズとして React Hook Form を用いたフォームのリファクタリング を行いました。
今回は、その リファクタリング後のコードをベースに機能追加 を行っていきます。

Part 1, Part 2 で作成したフォームは 「新規登録が前提」 となっており、初期値は 静的に設定 していました。
しかし、実際の開発では API から取得したデータを初期値として設定し、編集できるフォーム を作ることが求められます。

そこで今回は、「通信を行った結果を初期値として反映し、編集可能なフォーム」 を実装していきます。

React Hook Formを使用しないケースではフォームをマウントした時点でuseEffectを使用してAPI通信、レスポンスのオブジェクトから各入力欄のstateに対してsetするという動作でしたが、React-hook-formではどうなるのでしょうか、という点を検証したいと思います。
尚、ファイルアップロード機能の点に関しては別途記事を作成する予定です。

▼ これまでの記事

参考までに前回作成したコード(リファクタリング実施後)

型や定数の管理に関しては、前記事part2を参照していただければと思います。

iimport dayjs from "dayjs";
import { SubmitHandler, useForm } from "react-hook-form";
import { FormFieldItem } from "./types/formItem";
import FormField from "./components/FormField";
import FormStack from "./components/FormStack";
import {
  CONTACT_METHOD_OPTIONS,
  GENDER_OPTIONS,
  HOBBY_OPTIONS,
  INTEREST_OPTIONS,
  LIKE_COLOR_OPTIONS,
  PREFECTURE_OPTIONS,
  WORKS_OPTIONS,
} from "./constants/formOption";

type FormValues = {
  fullName: string; // フルネーム(入力必須)
  handleName: string; // ハンドルネーム(任意入力)
  age: number; // 年齢(数値)
  birthDate: string; // 生年月日(YYYY/MM/DD)
  gender: string; // 性別(ラジオボタン、選択必須)
  interestedIn: string; // 興味のあるジャンル(ラジオボタン)
  otherInterest: string; // 興味のあるジャンル("その他"を選択した場合)
  hobby: string[]; // 趣k味・特技(チェックボックス、任意)
  contactMethod: string[]; // 連絡方法(チェックボックス、選択必須)
  likeColor: string[]; // 好きな色(チェックボックス、2個まで選択可能)
  prefecture: string; // 都道府県(セレクトボックス、任意)
  works: string; // 職業(セレクトボックス、選択必須)
  profileImage: FileList | null; // プロフィール写真(1ファイル必須)
  coverImage: FileList | null; // カバー写真(任意)
  otherImage: FileList | null; // その他の写真(最大3ファイル、任意)
};

function App() {
  const defaultValues: FormValues = {
    fullName: "",
    handleName: "",
    age: 20,
    birthDate: "",
    gender: "0",
    interestedIn: "",
    otherInterest: "",
    hobby: [],
    contactMethod: [],
    likeColor: [],
    prefecture: "",
    works: "",
    profileImage: null,
    coverImage: null,
    otherImage: null,
  };

  const {
    register,
    handleSubmit,
    formState: { errors },
    watch,
    reset,
  } = useForm<FormValues>({ defaultValues });

  const selectedInterest = watch("interestedIn"); //「興味のあるジャンル」で
  const selectedInterestIsOther = selectedInterest === "other"; //「その他」を選んでいることを監視する

  console.log(errors);
  const formFieldItems: FormFieldItem[] = [
    {
      formName: "フルネーム(入力必須, 20文字以内)",
      formItem: (
        <input
          type="text"
          {...register("fullName", {
            required: "フルネームは必須です。",
            maxLength: { value: 20, message: "20文字以内で入力してください。" },
          })}
        />
      ),
      validationMessage: errors.fullName?.message,
    },
    {
      formName: "ハンドルネーム(任意入力)",
      formItem: <input type="text" {...register("handleName")} />,
      validationMessage: errors.handleName?.message,
    },
    {
      formName: "年齢(18~70歳まで)",
      formItem: (
        <input
          type="number"
          {...register("age", {
            required: "年齢は入力必須です",
            valueAsNumber: true,
            min: { value: 18, message: "18歳〜70歳までが利用可能です" },
            max: { value: 70, message: "18歳〜70歳までが利用可能です" },
          })}
        />
      ),
      validationMessage: errors.age?.message,
    },
    {
      formName: "生年月日(YYYY/MM/DD)",
      formItem: (
        <input
          type="date"
          {...register("birthDate", {
            required: "生年月日は必須です",
            setValueAs: (value) => dayjs(value).format("YYYY/MM/DD"),
          })}
        />
      ),
      validationMessage: errors.birthDate?.message,
    },
    {
      formName: "性別(選択必須)",
      formItem: (
        <FormStack>
          {GENDER_OPTIONS.map((item) => (
            <label key={item.value}>
              <input type="radio" value={item.value} {...register("gender", { required: "性別は必須です" })} />
              {item.text}
            </label>
          ))}
        </FormStack>
      ),
      validationMessage: errors.gender?.message,
    },
    {
      formName: "興味のあるジャンル(必須)",
      formItem: (
        <>
          <FormStack>
            {INTEREST_OPTIONS.map((item) => (
              <label key={item.value}>
                <input type="radio" value={item.value} {...register("interestedIn", { required: "必須項目です" })} />
                {item.text}
              </label>
            ))}
          </FormStack>
          {selectedInterestIsOther && (
            <>
              <input type="text" {...register("otherInterest", { required: "その他を選んだ場合は入力してください" })} />
              <p className="text-red-500">{errors.otherInterest?.message}</p>
            </>
          )}
        </>
      ),
      validationMessage: errors.interestedIn?.message,
    },
    {
      formName: "趣味・特技(任意入力)",
      formItem: (
        <FormStack>
          {HOBBY_OPTIONS.map((item) => (
            <label>
              <input type="checkbox" value={item.value} />
              {item.text}
            </label>
          ))}
        </FormStack>
      ),
      validationMessage: errors.hobby?.message,
    },
    {
      formName: "連絡方法(必須)",
      formItem: (
        <FormStack>
          {CONTACT_METHOD_OPTIONS.map((item) => (
            <label>
              <input
                type="checkbox"
                value={item.value}
                {...register("contactMethod", { required: "連絡方法は入力必須です" })}
              />
              {item.text}
            </label>
          ))}
        </FormStack>
      ),
      validationMessage: errors.contactMethod?.message,
    },
    {
      formName: "好きな色(2つまで選択可)",
      formItem: (
        <FormStack>
          {LIKE_COLOR_OPTIONS.map((item) => (
            <label key={item.value}>
              <input
                type="checkbox"
                value={item.value}
                {...register("likeColor", {
                  validate: (color) => color.length <= 2 || "2個まで選択可能です",
                })}
              />
              {item.text}
            </label>
          ))}
        </FormStack>
      ),
      validationMessage: errors.likeColor?.message,
    },
    {
      formName: "都道府県(任意)",
      formItem: (
        <select {...register("prefecture")}>
          <option value="">-- 都道府県を選択 --</option>
          {PREFECTURE_OPTIONS.map((item) => (
            <option key={item.value} value={item.value}>
              {item.text}
            </option>
          ))}
        </select>
      ),
      validationMessage: errors.prefecture?.message,
    },
    {
      formName: "現在の職業(必須)",
      formItem: (
        <select {...register("works")}>
          <option value="">-- 現在の職業を選択 --</option>
          {WORKS_OPTIONS.map((item) => (
            <option key={item.value} value={item.value}>
              {item.text}
            </option>
          ))}
        </select>
      ),
      validationMessage: errors.prefecture?.message,
    },
    {
      formName: "プロフィール写真(必須)",
      formItem: <input type="file" {...register("profileImage", { required: "プロフィール写真は必須です" })} />,
      validationMessage: errors.profileImage?.message,
    },
    {
      formName: "カバー写真(任意)",
      formItem: <input type="file" {...register("coverImage")} />,
      validationMessage: errors.coverImage?.message,
    },
    {
      formName: "その他の写真(3枚まで)",
      formItem: (
        <input
          type="file"
          multiple
          {...register("otherImage", {
            validate: (files) => {
              if (!files || files.length === 0) return true; // 未選択(null or 空配列)の場合はOK
              return files.length <= 3 || "任意写真は3枚までです"; // 3枚まで
            },
          })}
        />
      ),
      validationMessage: errors.otherImage?.message,
    },
  ];

  const onSubmit: SubmitHandler<FormValues> = (data) => {
    alert("クリックしたよ");
    console.log(data);
  };

  return (
    <main className="bg-gray-400 h-screen">
      <div className="flex flex-col gap-3 p-2">
        <h1 className="text-2xl">React-hook-formを使用したフォームサンプル集</h1>
        <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
          <FormField formFieldItems={formFieldItems} />
          <div className="flex gap-4">
            <button type="button" onClick={() => reset(defaultValues)} className="bg-white p-2.5 rounded-xl">
              入力内容をリセット
            </button>
            <button type="submit" className="bg-blue-400 p-2.5 rounded-xl">
              入力内容を送信
            </button>
          </div>
        </form>
      </div>
    </main>
  );
}

export default App;
export default App;

条件について

  • 通信はaxiosで行なって実際にデータを取得する
  • JSONファイルを作成してaxiosGETの実行を行う
  • フォーム(App.tsx)がマウントされた時点で通信開始する
  • ファイルアップロード周りは,レスポンスとして望ましいのがアップロード先URLなどのため、今回はコメントアウトして項目から省略

srcと同階層のpublicディレクトリに以下のresponse.jsonファイルを作成

※public でなくてもOK で、src と同階層のディレクトリ(例えば data/ や assets/)に response.json を置いて、axios.get() で適切なパスを指定すれば問題なく取得可能。
src下にjsonを置いてGETしようとすると、response.dataがjson形式でなくHTMLとして取得されてしまうため注意すること。

{
  "fullName": "山中太郎",
  "handleName": "ピーナッツ大臣",
  "age": 50,
  "birthDate": "2025/03/13",
  "gender": "0",
  "interestedIn": "other",
  "otherInterest": "深海魚",
  "hobby": [],
  "contactMethod": ["phone", "email"],
  "likeColor": ["blue", "yellow"],
  "prefecture": "1",
  "works": "0"
}

フォーム全てに適用するケースと部分的に適用するケースがあるらしい

  • 前データを反映させたい場合(defaultValuesを使用)
  • 一部のデータを反映させたい場合(setValueを使用)

defaultValuesを使用してすべてのデータを反映させてる場合のサンプルコード

import dayjs from "dayjs";
import { SubmitHandler, useForm } from "react-hook-form";
import { FormFieldItem } from "./types/formItem";
import FormField from "./components/FormField";
import FormStack from "./components/FormStack";
import {
  CONTACT_METHOD_OPTIONS,
  GENDER_OPTIONS,
  HOBBY_OPTIONS,
  INTEREST_OPTIONS,
  LIKE_COLOR_OPTIONS,
  PREFECTURE_OPTIONS,
  WORKS_OPTIONS,
} from "./constants/formOption";
import axios from "axios";
import { useEffect } from "react";

type FormValues = {
  fullName: string; // フルネーム(入力必須)
  handleName: string; // ハンドルネーム(任意入力)
  age: number; // 年齢(数値)
  birthDate: string; // 生年月日(YYYY/MM/DD)
  gender: string; // 性別(ラジオボタン、選択必須)
  interestedIn: string; // 興味のあるジャンル(ラジオボタン)
  otherInterest: string; // 興味のあるジャンル("その他"を選択した場合)
  hobby: string[]; // 趣k味・特技(チェックボックス、任意)
  contactMethod: string[]; // 連絡方法(チェックボックス、選択必須)
  likeColor: string[]; // 好きな色(チェックボックス、2個まで選択可能)
  prefecture: string; // 都道府県(セレクトボックス、任意)
  works: string; // 職業(セレクトボックス、選択必須)
  // profileImage: FileList | null; // プロフィール写真(1ファイル必須)
  // coverImage: FileList | null; // カバー写真(任意)
  // otherImage: FileList | null; // その他の写真(最大3ファイル、任意)
};

function App() {
  const defaultValues: FormValues = {
    fullName: "",
    handleName: "",
    age: 20,
    birthDate: "",
    gender: "0",
    interestedIn: "",
    otherInterest: "",
    hobby: [],
    contactMethod: [],
    likeColor: [],
    prefecture: "",
    works: "",
    // profileImage: null,
    // coverImage: null,
    // otherImage: null,
  };

  const {
    register,
    handleSubmit,
    formState: { errors },
    watch,
    reset,
  } = useForm<FormValues>({ defaultValues });

  const selectedInterest = watch("interestedIn"); //「興味のあるジャンル」で
  const selectedInterestIsOther = selectedInterest === "other"; //「その他」を選んでいることを監視する

  const formFieldItems: FormFieldItem[] = [
    {
      formName: "フルネーム(入力必須, 20文字以内)",
      formItem: (
        <input
          type="text"
          {...register("fullName", {
            required: "フルネームは必須です。",
            maxLength: { value: 20, message: "20文字以内で入力してください。" },
          })}
        />
      ),
      validationMessage: errors.fullName?.message,
    },
    {
      formName: "ハンドルネーム(任意入力)",
      formItem: <input type="text" {...register("handleName")} />,
      validationMessage: errors.handleName?.message,
    },
    {
      formName: "年齢(18~70歳まで)",
      formItem: (
        <input
          type="number"
          {...register("age", {
            required: "年齢は入力必須です",
            valueAsNumber: true,
            min: { value: 18, message: "18歳〜70歳までが利用可能です" },
            max: { value: 70, message: "18歳〜70歳までが利用可能です" },
          })}
        />
      ),
      validationMessage: errors.age?.message,
    },
    {
      formName: "生年月日(YYYY/MM/DD)",
      formItem: (
        <input
          type="date"
          {...register("birthDate", {
            required: "生年月日は必須です",
            setValueAs: (value) => dayjs(value).format("YYYY/MM/DD"),
          })}
        />
      ),
      validationMessage: errors.birthDate?.message,
    },
    {
      formName: "性別(選択必須)",
      formItem: (
        <FormStack>
          {GENDER_OPTIONS.map((item) => (
            <label key={item.value}>
              <input type="radio" value={item.value} {...register("gender", { required: "性別は必須です" })} />
              {item.text}
            </label>
          ))}
        </FormStack>
      ),
      validationMessage: errors.gender?.message,
    },
    {
      formName: "興味のあるジャンル(必須)",
      formItem: (
        <>
          <FormStack>
            {INTEREST_OPTIONS.map((item) => (
              <label key={item.value}>
                <input type="radio" value={item.value} {...register("interestedIn", { required: "必須項目です" })} />
                {item.text}
              </label>
            ))}
          </FormStack>
          {selectedInterestIsOther && (
            <>
              <input type="text" {...register("otherInterest", { required: "その他を選んだ場合は入力してください" })} />
              <p className="text-red-500">{errors.otherInterest?.message}</p>
            </>
          )}
        </>
      ),
      validationMessage: errors.interestedIn?.message,
    },
    {
      formName: "趣味・特技(任意入力)",
      formItem: (
        <FormStack>
          {HOBBY_OPTIONS.map((item) => (
            <label>
              <input type="checkbox" value={item.value} />
              {item.text}
            </label>
          ))}
        </FormStack>
      ),
      validationMessage: errors.hobby?.message,
    },
    {
      formName: "連絡方法(必須)",
      formItem: (
        <FormStack>
          {CONTACT_METHOD_OPTIONS.map((item) => (
            <label>
              <input
                type="checkbox"
                value={item.value}
                {...register("contactMethod", { required: "連絡方法は入力必須です" })}
              />
              {item.text}
            </label>
          ))}
        </FormStack>
      ),
      validationMessage: errors.contactMethod?.message,
    },
    {
      formName: "好きな色(2つまで選択可)",
      formItem: (
        <FormStack>
          {LIKE_COLOR_OPTIONS.map((item) => (
            <label key={item.value}>
              <input
                type="checkbox"
                value={item.value}
                {...register("likeColor", {
                  validate: (color) => color.length <= 2 || "2個まで選択可能です",
                })}
              />
              {item.text}
            </label>
          ))}
        </FormStack>
      ),
      validationMessage: errors.likeColor?.message,
    },
    {
      formName: "都道府県(任意)",
      formItem: (
        <select {...register("prefecture")}>
          <option value="">-- 都道府県を選択 --</option>
          {PREFECTURE_OPTIONS.map((item) => (
            <option key={item.value} value={item.value}>
              {item.text}
            </option>
          ))}
        </select>
      ),
      validationMessage: errors.prefecture?.message,
    },
    {
      formName: "現在の職業(必須)",
      formItem: (
        <select {...register("works")}>
          <option value="">-- 現在の職業を選択 --</option>
          {WORKS_OPTIONS.map((item) => (
            <option key={item.value} value={item.value}>
              {item.text}
            </option>
          ))}
        </select>
      ),
      validationMessage: errors.prefecture?.message,
    },
    // {
    //   formName: "プロフィール写真(必須)",
    //   formItem: <input type="file" {...register("profileImage", { required: "プロフィール写真は必須です" })} />,
    //   validationMessage: errors.profileImage?.message,
    // },
    // {
    //   formName: "カバー写真(任意)",
    //   formItem: <input type="file" {...register("coverImage")} />,
    //   validationMessage: errors.coverImage?.message,
    // },
    // {
    //   formName: "その他の写真(3枚まで)",
    //   formItem: (
    //     <input
    //       type="file"
    //       multiple
    //       {...register("otherImage", {
    //         validate: (files) => {
    //           if (!files || files.length === 0) return true; // 未選択(null or 空配列)の場合はOK
    //           return files.length <= 3 || "任意写真は3枚までです"; // 3枚まで
    //         },
    //       })}
    //     />
    //   ),
    //   validationMessage: errors.otherImage?.message,
    // },
  ];

  const onSubmit: SubmitHandler<FormValues> = (data) => {
    alert("クリックしたよ");
    console.log(JSON.stringify(data));
  };

  //--------------追加された内容-------------------

  const getData = async () => {
    try {
      // jsonを直接取得する際はjsonファイルの置き場所をsrcと同階層で置かないと、取得に失敗する。
      const response = await axios.get("../public/response.json");
      reset(response.data);
    } catch (error) {
      alert("取得に失敗しました");
    }
  };

  useEffect(() => {
    getData();
  }, []);

  //--------------追加された内容-------------------

  return (
    <main className="bg-gray-400 h-screen">
      <div className="flex flex-col gap-3 p-2">
        <h1 className="text-2xl">React-hook-formを使用したフォームサンプル集</h1>
        <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
          <FormField formFieldItems={formFieldItems} />
          <div className="flex gap-4">
            <button type="button" onClick={() => reset(defaultValues)} className="bg-white p-2.5 rounded-xl">
              入力内容をリセット
            </button>
            <button type="submit" className="bg-blue-400 p-2.5 rounded-xl">
              入力内容を送信
            </button>
          </div>
        </form>
      </div>
    </main>
  );
}

export default App;

useFormから分割代入で呼び出しているresetの引数にresponse.dataを使用してあげればOK。
思いの外簡単でした。実際にマウント時に通信して、value値が反映された画面となります。
スクリーン ショット 2025-03-01 に 16.50.11 午後.png

取得しているデータは変えずに、フルネームとハンドルネームだけを適用してみる(部分的な更新)

こちらもほぼ上記と変わらず、useFormから分割代入で追加した「setValues」を使用することが可能でした。

  • set("formValueのキー名", 適用したい値)
  • この書き方をgetDataの関数ないで実行するのみです。
import dayjs from "dayjs";
import { SubmitHandler, useForm } from "react-hook-form";
import { FormFieldItem } from "./types/formItem";
import FormField from "./components/FormField";
import FormStack from "./components/FormStack";
import {
  CONTACT_METHOD_OPTIONS,
  GENDER_OPTIONS,
  HOBBY_OPTIONS,
  INTEREST_OPTIONS,
  LIKE_COLOR_OPTIONS,
  PREFECTURE_OPTIONS,
  WORKS_OPTIONS,
} from "./constants/formOption";
import axios from "axios";
import { useEffect } from "react";

type FormValues = {
  fullName: string; // フルネーム(入力必須)
  handleName: string; // ハンドルネーム(任意入力)
  age: number; // 年齢(数値)
  birthDate: string; // 生年月日(YYYY/MM/DD)
  gender: string; // 性別(ラジオボタン、選択必須)
  interestedIn: string; // 興味のあるジャンル(ラジオボタン)
  otherInterest: string; // 興味のあるジャンル("その他"を選択した場合)
  hobby: string[]; // 趣k味・特技(チェックボックス、任意)
  contactMethod: string[]; // 連絡方法(チェックボックス、選択必須)
  likeColor: string[]; // 好きな色(チェックボックス、2個まで選択可能)
  prefecture: string; // 都道府県(セレクトボックス、任意)
  works: string; // 職業(セレクトボックス、選択必須)
  // profileImage: FileList | null; // プロフィール写真(1ファイル必須)
  // coverImage: FileList | null; // カバー写真(任意)
  // otherImage: FileList | null; // その他の写真(最大3ファイル、任意)
};

function App() {
  const defaultValues: FormValues = {
    fullName: "",
    handleName: "",
    age: 20,
    birthDate: "",
    gender: "0",
    interestedIn: "",
    otherInterest: "",
    hobby: [],
    contactMethod: [],
    likeColor: [],
    prefecture: "",
    works: "",
    // profileImage: null,
    // coverImage: null,
    // otherImage: null,
  };

  const {
    register,
    handleSubmit,
    formState: { errors },
    watch,
    reset,
    setValue, //追加
  } = useForm<FormValues>({ defaultValues });

  const selectedInterest = watch("interestedIn"); //「興味のあるジャンル」で
  const selectedInterestIsOther = selectedInterest === "other"; //「その他」を選んでいることを監視する

  const formFieldItems: FormFieldItem[] = [
    {
      formName: "フルネーム(入力必須, 20文字以内)",
      formItem: (
        <input
          type="text"
          {...register("fullName", {
            required: "フルネームは必須です。",
            maxLength: { value: 20, message: "20文字以内で入力してください。" },
          })}
        />
      ),
      validationMessage: errors.fullName?.message,
    },
    {
      formName: "ハンドルネーム(任意入力)",
      formItem: <input type="text" {...register("handleName")} />,
      validationMessage: errors.handleName?.message,
    },
    {
      formName: "年齢(18~70歳まで)",
      formItem: (
        <input
          type="number"
          {...register("age", {
            required: "年齢は入力必須です",
            valueAsNumber: true,
            min: { value: 18, message: "18歳〜70歳までが利用可能です" },
            max: { value: 70, message: "18歳〜70歳までが利用可能です" },
          })}
        />
      ),
      validationMessage: errors.age?.message,
    },
    {
      formName: "生年月日(YYYY/MM/DD)",
      formItem: (
        <input
          type="date"
          {...register("birthDate", {
            required: "生年月日は必須です",
            setValueAs: (value) => dayjs(value).format("YYYY/MM/DD"),
          })}
        />
      ),
      validationMessage: errors.birthDate?.message,
    },
    {
      formName: "性別(選択必須)",
      formItem: (
        <FormStack>
          {GENDER_OPTIONS.map((item) => (
            <label key={item.value}>
              <input type="radio" value={item.value} {...register("gender", { required: "性別は必須です" })} />
              {item.text}
            </label>
          ))}
        </FormStack>
      ),
      validationMessage: errors.gender?.message,
    },
    {
      formName: "興味のあるジャンル(必須)",
      formItem: (
        <>
          <FormStack>
            {INTEREST_OPTIONS.map((item) => (
              <label key={item.value}>
                <input type="radio" value={item.value} {...register("interestedIn", { required: "必須項目です" })} />
                {item.text}
              </label>
            ))}
          </FormStack>
          {selectedInterestIsOther && (
            <>
              <input type="text" {...register("otherInterest", { required: "その他を選んだ場合は入力してください" })} />
              <p className="text-red-500">{errors.otherInterest?.message}</p>
            </>
          )}
        </>
      ),
      validationMessage: errors.interestedIn?.message,
    },
    {
      formName: "趣味・特技(任意入力)",
      formItem: (
        <FormStack>
          {HOBBY_OPTIONS.map((item) => (
            <label>
              <input type="checkbox" value={item.value} />
              {item.text}
            </label>
          ))}
        </FormStack>
      ),
      validationMessage: errors.hobby?.message,
    },
    {
      formName: "連絡方法(必須)",
      formItem: (
        <FormStack>
          {CONTACT_METHOD_OPTIONS.map((item) => (
            <label>
              <input
                type="checkbox"
                value={item.value}
                {...register("contactMethod", { required: "連絡方法は入力必須です" })}
              />
              {item.text}
            </label>
          ))}
        </FormStack>
      ),
      validationMessage: errors.contactMethod?.message,
    },
    {
      formName: "好きな色(2つまで選択可)",
      formItem: (
        <FormStack>
          {LIKE_COLOR_OPTIONS.map((item) => (
            <label key={item.value}>
              <input
                type="checkbox"
                value={item.value}
                {...register("likeColor", {
                  validate: (color) => color.length <= 2 || "2個まで選択可能です",
                })}
              />
              {item.text}
            </label>
          ))}
        </FormStack>
      ),
      validationMessage: errors.likeColor?.message,
    },
    {
      formName: "都道府県(任意)",
      formItem: (
        <select {...register("prefecture")}>
          <option value="">-- 都道府県を選択 --</option>
          {PREFECTURE_OPTIONS.map((item) => (
            <option key={item.value} value={item.value}>
              {item.text}
            </option>
          ))}
        </select>
      ),
      validationMessage: errors.prefecture?.message,
    },
    {
      formName: "現在の職業(必須)",
      formItem: (
        <select {...register("works")}>
          <option value="">-- 現在の職業を選択 --</option>
          {WORKS_OPTIONS.map((item) => (
            <option key={item.value} value={item.value}>
              {item.text}
            </option>
          ))}
        </select>
      ),
      validationMessage: errors.prefecture?.message,
    },
    // {
    //   formName: "プロフィール写真(必須)",
    //   formItem: <input type="file" {...register("profileImage", { required: "プロフィール写真は必須です" })} />,
    //   validationMessage: errors.profileImage?.message,
    // },
    // {
    //   formName: "カバー写真(任意)",
    //   formItem: <input type="file" {...register("coverImage")} />,
    //   validationMessage: errors.coverImage?.message,
    // },
    // {
    //   formName: "その他の写真(3枚まで)",
    //   formItem: (
    //     <input
    //       type="file"
    //       multiple
    //       {...register("otherImage", {
    //         validate: (files) => {
    //           if (!files || files.length === 0) return true; // 未選択(null or 空配列)の場合はOK
    //           return files.length <= 3 || "任意写真は3枚までです"; // 3枚まで
    //         },
    //       })}
    //     />
    //   ),
    //   validationMessage: errors.otherImage?.message,
    // },
  ];

  const onSubmit: SubmitHandler<FormValues> = (data) => {
    alert("クリックしたよ");
    console.log(JSON.stringify(data));
  };

  //--------------追加された内容-------------------

  const getData = async () => {
    try {
      // jsonを直接取得する際はjsonファイルの置き場所をsrc下でないところに置かないと、取得に失敗する。
      const response = await axios.get<FormValues>("../public/response.json");
      setValue("fullName", response.data.fullName);
      setValue("handleName", response.data.handleName);
    } catch (error) {
      alert("取得に失敗しました");
    }
  };

  useEffect(() => {
    getData();
  }, []);

  //--------------追加された内容-------------------

  return (
    <main className="bg-gray-400 h-screen">
      <div className="flex flex-col gap-3 p-2">
        <h1 className="text-2xl">React-hook-formを使用したフォームサンプル集</h1>
        <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
          <FormField formFieldItems={formFieldItems} />
          <div className="flex gap-4">
            <button type="button" onClick={() => reset(defaultValues)} className="bg-white p-2.5 rounded-xl">
              入力内容をリセット
            </button>
            <button type="submit" className="bg-blue-400 p-2.5 rounded-xl">
              入力内容を送信
            </button>
          </div>
        </form>
      </div>
    </main>
  );
}

export default App;

あらかじめ設定されている年齢(age)は20が適用され、
フルネームとハンドルネームに限っては格納済みのjsonファイルと同等の値
その他は全て未入力状態となり、適切な実装ができました。

スクリーン ショット 2025-03-01 に 17.06.17 午後.png

最後に

state管理で自分で書いた方が楽なんじゃないかが学習する前の本音でしたが、
学習を進めてみるとたしかに色々書き方はシンプルな感じもかなりしてきました。
ただ、便利である反面基本的なstateを使用した実装の仕方がわからないままこのライブラリに手を出してしまうのはやっぱりちょっと早そう......どちらのケースもできた方が良いですね。
また、今回の記事を持って自分としてはおおよそ基本的な構造は抑えられたかなというにnです、あとは必要な要件とか実際に現場で出てきたら調べればいいかなと。ありがとうございました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?