🪝

react-hook-formの使い方

2024/08/05に公開
2

こんにちは。Web デザイナーのツーさんです 😁。今までは WordPress や CSS の記事を書いていたのですが、ありがたいことに少し前からフロントエンドのお仕事が増えてきたので、今回からフロントエンド関連の記事も書いていこうと思います。僕はフロントエンドのお仕事では React や NextJS をよく使っています。

今回は React(Next) でのフォーム作成ライブラリの定番とも言える 「react-hook-form」 の使い方をご紹介します。
※ Zod や Yup などは本記事では使用していません。

記事内に掲載しているソースコードは Github でも確認できます。

https://github.com/twosun-8-git/react-hook-form

react-hook-form とは 🤔

react-hook-form とは React(NextJS) で作成するアプリケーションのフォーム処理を簡素化し、効率的に管理するためのライブラリです。主に次のような特徴があります。

  • TypeScript との相性が良い
  • 不要な再レンダリングを最小限に抑えるのでパフォーマンスに優れている
  • バリデーションの設定が簡単

公式サイトはこちら → https://react-hook-form.com/

開発環境 🛠️

では、開発環境を準備していきたいと思います。今回は Next を利用します。Node や Next などの詳しいバージョンは下記のようになっています。

Node や Next のバージョン

  • Node.js(18.20.4)
  • React(^18)
  • Next(14.2.5)
  • react-hook-form(^7.52.1)
  • MUI(5.16.6)

では、まず create-next-app で Next アプリの雛形を作ります。アプリ名は my-react-hook-form としました。

npx create-next-app

/ /下記のように設定
What is your project named?  my-react-hook-form
Would you like to use TypeScript?  Yes
Would you like to use ESLint?  Yes
Would you like to use Tailwind CSS?  No
Would you like to use `src/` directory?  Yes
Would you like to use App Router? (recommended)  Yes
Would you like to customize the default import alias (@/*)?  No

続いてreact-hook-form をインストールします。

npm install react-hook-form

これで準備ができました。
今回は検証用として 「ユーザーのプロフィールページ」 を作成していきます。仕様やフォームの入力項目などは下記をご参照ください。


仕様

  • バリデーションあり。入力項目によって適切なエラーメッセージを表示
  • フォームのリセットができる
  • フォームの内容を変更後に送信せずページを離脱しようとしたらアラートを表示する
  • 送信状態や送信結果が UI 上で把握できる

入力項目

項目 タイプ バリデーション
フルネーム Text 必須
性別 Select 必須
自己紹介 Textarea 任意 (入力された場合は 5 文字以上 15 文字以下であること)
お知らせ radio 必須
利用規約同意 Checkbox 必須

なお、今回は CSS については react-hook-form と関係ないので解説はしません。CSS が必要な方は下記からコピペして利用してください。

CSS

基本的な使い方 🔍

まずは、今回の基本となるソースコードです。ここではバリデーションの動作について確認したいので送信状況や送信結果などは表示しないシンプルな状態にしています。

"use client";
import { useEffect } from "react";
import { Controller, useForm, SubmitHandler } from "react-hook-form";

const Gender = {
  empty: "",
  female: "female",
  male: "male",
  other: "other",
} as const; // as const でreadonlyにする

const NewsLatter = {
  receive: "receive",
  reject: "reject",
} as const;

// Union型を生成
type Gender = (typeof Gender)[keyof typeof Gender];
type NewsLatter = (typeof NewsLatter)[keyof typeof NewsLatter];

// フォームで使用する型を生成
type Inputs = {
  fullName: string;
  gender: Gender;
  comment?: string;
  newsLatter: NewsLatter;
  agree: boolean;
};

export default function Page() {
  // 初期値
  const defaultValues = {
    fullName: "",
    gender: Gender.empty,
    comment: "",
    newsLatter: NewsLatter.receive,
    agree: false,
  };

  // react hook from の初期設定
  const { control, handleSubmit, reset } = useForm<Inputs>({
    defaultValues,
  });

  // フォーム送信処理
  const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);

  // フォームリセット処理
  const handleReset = () => {
    reset();
  };

  return (
    <div>
      <p className="breadcrumb">[ basic ]</p>
      <form onSubmit={handleSubmit(onSubmit)} className="form">
        <Controller
          name="fullName"
          rules={{ required: "名前が入力されてないようです。" }}
          control={control}
          render={({ field, fieldState }) => (
            <div
              className={`form-group ${!!fieldState.error ? `is-error` : ``}`}
            >
              <label htmlFor="fullName">名前</label>
              <div>
                <input id="fullName" type="text" {...field} />
                {fieldState.error && (
                  <span className="error-message">
                    {fieldState.error.message}
                  </span>
                )}
              </div>
            </div>
          )}
        />
        <Controller
          name="gender"
          rules={{ required: "性別が選択されてないようです。" }}
          control={control}
          render={({ field, fieldState }) => (
            <div
              className={`form-group ${!!fieldState.error ? `is-error` : ``}`}
            >
              <label htmlFor="gender">性別</label>
              <div>
                <select id="gender" {...field}>
                  <option value={Gender.empty}></option>
                  <option value={Gender.female}>女性</option>
                  <option value={Gender.male}>男性</option>
                  <option value={Gender.other}>無回答</option>
                </select>
                {fieldState.error && (
                  <span className="error-message">
                    {fieldState.error.message}
                  </span>
                )}
              </div>
            </div>
          )}
        />
        <Controller
          name="comment"
          rules={{
            minLength: { value: 3, message: "3文字以上で入力してください" },
            maxLength: { value: 10, message: "10文字以下で入力してください" },
          }}
          control={control}
          render={({ field, fieldState }) => (
            <div
              className={`form-group ${!!fieldState.error ? `is-error` : ``}`}
            >
              <label htmlFor="comment">コメント</label>
              <div>
                <textarea id="comment" {...field} />
                {fieldState.error && (
                  <span className="error-message">
                    {fieldState.error.message}
                  </span>
                )}
              </div>{" "}
            </div>
          )}
        />
        <Controller
          name="newsLatter"
          rules={{
            required: "お知らせの受け取りについて選択してください",
          }}
          control={control}
          render={({ field, fieldState }) => (
            <div
              className={`form-group ${!!fieldState.error ? `is-error` : ``}`}
            >
              <label htmlFor="newsLatter">お知らせ</label>
              <div>
                <div className="radio-group">
                  <label>
                    <input
                      type="radio"
                      {...field}
                      value={NewsLatter.receive}
                      checked={field.value === NewsLatter.receive}
                    />
                    受け取る
                  </label>
                  <label>
                    <input
                      type="radio"
                      {...field}
                      value={NewsLatter.reject}
                      checked={field.value === NewsLatter.reject}
                    />
                    受け取らない
                  </label>
                </div>
                {fieldState.error && (
                  <span className="error-message">
                    {fieldState.error.message}
                  </span>
                )}
              </div>
            </div>
          )}
        />
        <Controller
          name="agree"
          rules={{ required: "チェックボックスをチェックしてください" }}
          control={control}
          render={({ field: { value, ...rest }, fieldState }) => (
            <div
              className={`form-group ${!!fieldState.error ? `is-error` : ``}`}
            >
              <div>
                <label>
                  <input type="checkbox" checked={value} {...rest} />{" "}
                  <span>利用規約に同意する</span>
                </label>
                {fieldState.error && (
                  <span className="error-message">
                    {fieldState.error.message}
                  </span>
                )}
              </div>
            </div>
          )}
        />
        <div className="button-group">
          <button type="submit">送信</button>
          <button type="button" onClick={handleReset}>
            リセット
          </button>
        </div>
      </form>
    </div>
  );
}

動作確認

動作確認のためにnpm run devで開発サーバーを起動します。

npm run dev

起動したらフォームの送信やリセット、バリデーションを試してみます。下記はバリデーションエラーと送信時のキャプチャです。

バリデーション動作確認
バリデーション動作確認

サブミット動作確認
サブミット動作確認(コンソールに表示)

意図した通りに動いているので動作は問題ありません。
では解説していきます。

react-hook-form の初期設定

まずは初期設定です。useForm<Inputs> で型を指定していることで type Inputs で予め定義したデータ構造に基づいてフォームの状態を管理することができます。同時に defaultValues で初期値もセットしておきます。

// 初期値
const defaultValues = {
  fullName: "",
  gender: Gender.empty,
  comment: "",
  newsLatter: NewsLatter.receive,
  agree: false,
};

// react-hook-formの初期設定
const { control, handleSubmit, reset } = useForm<Inputs>({
  defaultValues,
});

そして useForm から 「control, handleSubmit, reset」 を受け取ります。これらは後ほど使用します。

名称 説明
control フォームの各フィールドを react-hook-form に登録しそれらの状態を管理するために使用されます。
handleSubmit フォームの送信処理を管理する関数。
reset フォームの状態をリセットする関数。

フォームの送信とリセット

続いてフォームの送信とリセット用の関数を作成します。送信用の関数(onSubmit)は<form onSubmit={handleSubmit(onSubmit)}>のように handleSubmit の引数として指定します。まずはバリデーションの確認のみを行いたいので今は送信したデータをコンソールに表示するだけにしています。

// フォーム送信処理
const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);

// フォームリセット処理(defaultValueの値に戻る)
const handleReset = () => {
  reset();
};

入力項目ごとのバリデーション

では、肝心のバリデーションの部分です。
react-hook-form から import した Controller コンポーネントを利用してフォームのパーツを作っていきます。

その前に少し補足です。<input defaultValue="test" {...register("fullName")} />のように書けば Controller コンポーネント を利用しなくても作ることはできますが、今回は他の UI ライブラリと組み合わせる可能性も考慮し Controller コンポーネント を利用しています。

{...register}の書き方が好みの方は下記をご参考ください。
https://react-hook-form.com/get-started

ではわかりやすい<input type="text" />を例に挙げて解説していきます。

<Controller
  name="fullName" // type Inputs内に存在していること
  rules={{ required: "名前が入力されてないようです。" }} // バリデーションエラー時のテキスト
  control={control}
  render={({ field, fieldState }) => (
    // !!fieldState.errorでエラー時のクラスを追加
    <div className={`form-group ${!!fieldState.error ? `is-error` : ``}`}>
      <label htmlFor="fullName">名前</label>
      <div>
        <input id="fullName" type="text" {...field} /> // fieldを展開
        {fieldState.error && (
          <span className="error-message">{fieldState.error.message}</span> // エラーメッセージ
        )}
      </div>
    </div>
  )}
/>

下記は Controller コンポーネントの props の解説です。

props 説明
name "fullName", "gender", "comment"... type Inputs 内に存在すること。無い場合はエラーになる
rules required, maxLength, minLength... バリデーションの設定。必須や入力されるデータの最小数や最大数を設定できる
control control control を設定することで react-hook-form の管理対象となる
render DOM 実際に表示(レンダリング)する DOM を設定する

続いて render の { field, fieldState } についても説明します。
field には name, value, onChange が格納されておりそれを展開して利用しています。

const { name, value, onChange, onBlur, ref } = field;

/** 展開して利用 */
// <input type="text" {...field} />

/** 展開しなかった場合の一例 */
// <input type="text" name={name} value={value} onChange={onChange} onBlur={onBlur} ref={ref} />

fieldStateinValid, isTouched, isDirty, error が格納されています。

名称 説明
inValid エラーが検出されているか
isTouched 操作されているか(focus / blur イベントが発生しているか)
isDirty 変更されているか
error エラー情報

checkbox と radio ボタンでの field の使い方

<input type="text" /><textarea>{...field} になっていますが、 checkboxradio ボタンはちょっと記述が違いますね。この点について少し説明したいと思います。

checkbox について

<input type="checkbox" {...field} /> とすると TypeScript の型チェックに引っかかりエディターに下記のようなエラーメッセージが出ると思います。

型 '{ onChange: (...event: any[]) => void; onBlur: Noop; value: boolean; disabled?: boolean | undefined; name: "agree"; ref: RefCallBack; type: "checkbox"; }' を型 'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>' に割り当てることはできません。
型 '{ onChange: (...event: any[]) => void; onBlur: Noop; value: boolean; disabled?: boolean | undefined; name: "agree"; ref: RefCallBack; type: "checkbox"; }' を型 'InputHTMLAttributes<HTMLInputElement>' に割り当てることはできません。
プロパティ 'value' の型に互換性がありません。
型 'boolean' を型 'string | number | readonly string[] | undefined' に割り当てることはできません。

下記のメッセージに注目です。

型 'boolean' を型 'string | number | readonly string[] | undefined' に割り当てることはできません。

これは <input type="checkbox">value に期待される型は "string | number | readonly string[] | undefined" であるのに対して、react-hook-form が提供する value の型は "boolean" であるため型の不一致が起きていることが原因です。

このような場合は下記のように value他のプロパティ(rest) を分離して利用することで解決します。

// value と rest でプロパティを分離。valueはチェックされた場合のみ true になる。
<input type="checkbox" checked={value} {...rest} />

radio ボタンについて

<input type="radio" {...field} /> と書いてもエディタでは特にエラーメッセージは表示されません。ですが、その場合どちらの radio ボタンも値が defaultValues で設定した NewsLatter.receive になってしまいます。

  const defaultValues = {
    fullName: "",
    gender: Gender.empty,
    comment: "",
    newsLatter: NewsLatter.receive,
    agree: false,
  };

そのため下記のように {...field} で展開した後に value={NewsLatter.receive}"value を上書き" しています。そして checked={field.value === NewsLatter.receive} で選択状態を UI に反映しています。

<label>
  <input
    type="radio"
    {...field}
    value={NewsLatter.receive} // value "receive" に上書き
    checked={field.value === NewsLatter.receive}
  />
  受け取る
</label>

<label>
  <input
    type="radio"
    {...field}
    value={NewsLatter.reject} // value "reject" に上書き
    checked={field.value === NewsLatter.reject}
  />
  受け取らない
</label>

以上。checkbox と radio ボタンについての補足でした。

フォームをカスタマイズして使いやすくする 👍

先ほどのフォームでは送信やリセットは行えますが、最低限の機能だけなのでユーザーフレンドリーとは言えません。下記の点を追加してフォームを使いやすくしたいと思います。

  • バリデーションエラーの時は「送信」ボタンを押せないようにする (disabled)
  • 送信前にページを離脱しようとしたらアラートを表示する (beforeunload)
  • フォームの送信状況や送信結果を UI に反映する
  • フォームの送信に失敗した場合はリトライできるようにする

react hook from の設定を変更

まずは、送信状況やバリデーションなどの状況を取得するために react-hook-from の設定を変更します。

// react hook from の初期設定
const {
  control,
  handleSubmit,
  reset,
+  setError,
+  clearErrors,
+  formState: {
+    isDirty,
+    isValid,
+    isSubmitting,
+    isSubmitted,
+    isSubmitSuccessful,
+    errors,
+  },
} = useForm<Inputs>({
  defaultValues,
+  mode: "all",
});

「control, handleSubmit, reset」 以外にも様々な設定を追加しました。

名称 説明
setError 手動でエラーを設定できる
clearErrors エラーをクリア
isDirty フォームの内容に変更があったか (default: false)
isValid バリデーション検証結果 (default: false)
isSubmitting フォームの送信中であるか (default: false)
isSubmitted フォームが送信されたか (default: false)
isSubmitSuccessful フォームが正常に送信されたか (default: false)
errors エラー情報

defaultValues の下に "mode" が追加されていますね。 "mode" では バリデーションチェックを行うタイミングを指定することができます。

名称 説明
onSubmit フォームが送信される時(デフォルト)
onChange 入力要素の内容が変更された時
onBlur 入力要素からフォーカスが外れた時
all 上記全て

今回は "all" を指定していますが、フォームのパーツの数が多すぎたりするとパフォーマンスを悪化させる可能性があるのでご注意ください。

フォームの送信処理を変更

次にフォームの送信処理を変更します。成功時と失敗時を再現するためにconst isSuccess = Math.random() < 0.5;を利用してランダムに成功と失敗を実行しています。
フォーム送信時には最初にclearErrors("root")でエラー内容をクリアし、送信失敗の時は catch 内でsetErrorを使ってエラーをセットしています。

エラーが拾えるようになったことでフォーム送信の成功と失敗が把握できるようになりました。
これで状況に合わせて適切な UI を表示することができます。

  // フォーム送信処理
-  const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);

+  const onSubmit: SubmitHandler<Inputs> = async (data) => {
+    clearErrors("root");
+    try {
+      await new Promise<void>((resolve, reject) => {
+        setTimeout(() => {
+          // 成功と失敗をランダムに実行
+          const isSuccess = Math.random() < 0.5;
+          if (isSuccess) {
+            console.log("送信成功:", data);
+            // reset(); 本来ならフォーム送信成功後はフォームをリセット
+            resolve();
+          } else {
+            console.log("送信失敗");
+            reject(new Error("送信に失敗しました"));
+          }
+        }, 1500);
+      });
+    } catch (error) {
+      console.error("エラー:", error);
+      setError("root.serverError", {
+        type: "manual",
+        message: "送信に失敗しました。もう一度お試しください。",
+      });
+    }
+  };

バリデーションエラー時は「送信」ボタンを押せないようにする(disabled)

送信ボタンに disabled を追加しクリックの制御を行います。
isDirtyisValid を追加しいずれかが false の場合は disabled が適用されクリックできなくなります。

- <button type="submit">送信</button>
+ <button type="submit" disabled={!isDirty || !isValid}>送信</button>

送信前にページを離脱しようとしたらアラートを表示する(beforeunload)

次に beforeunload を利用して送信前にページを離脱しようとした時の処理を追加します。

  // フォームリセット処理
  const handleReset = () => {
    reset();
  };

+  // フォーム保存前離脱のアラート
+  useEffect(() => {
+    const handleBeforeUnload = (e: BeforeUnloadEvent) => { // BeforeUnloadEventはTypeScript の型定義のためimport不要
+      if (isDirty) {
+        e.preventDefault();
+      }
+    };
+
+    window.addEventListener("beforeunload", handleBeforeUnload);
+
+    return () => {
+      window.removeEventListener("beforeunload", handleBeforeUnload);
+    };
+  }, [isDirty]);

useEffect の依存配列に isDirty を指定し true の時にのみ beforeunload イベントのデフォルト動作を阻止します。これにより、"フォームの内容が変更されたが送信されていない状態でページを離脱" しようとするとアラートが表示されます。

フォームの送信状況や送信結果を表示する

先ほど 「 isSubmitting, isSubmitted, isSubmitSuccessful 」 を追加してありますのでこれらを使ってフォームの送信状況や送信結果を UI に表示します。

  <form onSubmit={handleSubmit(onSubmit)} className="form">

+  {isSubmitting && <div className="result">送信中</div>}
+  {!isSubmitting && isSubmitted && (
+    <div className={`result ${isSubmitSuccessful ? `is-success` : `is-failed`}`}>
+      {isSubmitSuccessful ? "送信成功" : "送信失敗"}
+    </div>
+  )}

  <Controller
    name="fullName"

フォームの送信に失敗した場合はリトライできるようにする

次にフォームの送信が失敗した場合は再送信できるようにリトライボタンを追加します。
先ほど onSubmit関数catch ブロックで setError を使用してエラーをセットしましたよね。それをここで利用します。

        {!isSubmitting && isSubmitted && (
          <div className={`result ${ isSubmitSuccessful ? `is-success` : `is-failed` }`}>
            {isSubmitSuccessful ? "送信成功" : "送信失敗"}
+            {errors.root?.serverError && (
+              <button onClick={() => handleSubmit(onSubmit)}>リトライ</button>
+            )}
          </div>
        )}

ここまでできたら npm run dev を実行し再度動作を確認してみましょう。

npm run dev

下記は実際の UI のキャプチャです。

送信中の表示
送信中の表示

送信失敗の表示
送信失敗の表示

送信成功の表示
送信成功の表示

バリデーションチェックのタイミングと isValid について 😵‍💫

react-hook-form はバリデーションチェックを行うタイミングはデフォルトでは onSubmit(フォーム送信時) となっています。

特に問題ないと思いますが 送信ボタンに disabled={!isValid}を設定 する時には注意した方がいいかもしれません。理由を説明します。ちょっと下記の画像を見てみましょう。
バリデーションとonSubmit

「名前、性別、コメント、お知らせ、利用規約」 全て設定されていますが 「送信ボタン」が押せません。エラーメッセージも表示されていません。 なぜでしょうか?

実は "コメントは3文字以上" のバリデーションがかかっているため、上記の画像では 2 文字のためバリデーションエラーが起こっているのです。

バリデーションエラーなので isValidfalse になり送信ボタンの disabled が解除されずクリックすることができません。

// isValid は false のまま
<button type="submit" disabled={!isValid}>
  送信
</button>

そして、送信ボタンがクリックできないので "1 度もバリデーションエラーのメッセージが表示されない。" とゆう状態になっています。
このままではユーザーは 「なぜ送信ボタンが押せないのか?」 への解決の糸口さえ見つけることができず困ってしまいます。

こんな時は "mode" を onChange や onBlurなどに変更してバリデーションチェックのタイミングを変更してあげれば解決できます。

  const {
    control,
    handleSubmit,
    reset,
    formState: { isValid },
  } = useForm<Inputs>({
    defaultValues,
    // 内容が変更されたらバリデーションチェックを行いエラーメッセージをすぐ表示する
+    mode: "onChange"
  });

バリデーションチェックのタイミングとバリデーションルールの組み合わせによってはユーザーが混乱する可能性がありますので、必ずバリデーションエラー時の挙動も確認するようにしておきましょう。

MUI で見た目を変更する 🐦

先ほどまでは素の HTML でフォームを作成しました。今度は MUI(Material UI) と組み合わせてフォームを作成してみます。
※仕様や設定項目、バリデーションルールなどに変更ありません。

MUI のインストール方法は下記をご参考ください。
https://mui.com/material-ui/getting-started/installation/

ソースコード

"use client";
import { useEffect } from "react";
import { Controller, useForm, SubmitHandler } from "react-hook-form";
import {
  TextField,
  Select,
  MenuItem,
  FormControl,
  InputLabel,
  FormControlLabel,
  Radio,
  RadioGroup,
  Checkbox,
  Button,
  Typography,
  Box,
  Container,
  CircularProgress,
  Stack,
  Alert,
} from "@mui/material";

const Gender = {
  empty: "",
  female: "female",
  male: "male",
  other: "other",
} as const;

const NewsLatter = {
  receive: "receive",
  reject: "reject",
} as const;

type Gender = (typeof Gender)[keyof typeof Gender];
type NewsLatter = (typeof NewsLatter)[keyof typeof NewsLatter];

type Inputs = {
  fullName: string;
  gender: Gender;
  comment?: string;
  newsLatter: NewsLatter;
  agree: boolean;
};

export default function Page() {
  const defaultValues = {
    fullName: "",
    gender: Gender.empty,
    comment: "",
    newsLatter: NewsLatter.receive,
    agree: false,
  };

  const {
    control,
    handleSubmit,
    reset,
    setError,
    clearErrors,
    formState: {
      isDirty,
      isValid,
      isSubmitting,
      isSubmitted,
      isSubmitSuccessful,
      errors,
    },
  } = useForm<Inputs>({
    defaultValues,
    mode: "all",
  });

  const onSubmit: SubmitHandler<Inputs> = async (data) => {
    clearErrors("root");
    try {
      await new Promise<void>((resolve, reject) => {
        setTimeout(() => {
          const isSuccess = Math.random() < 0.5;
          if (isSuccess) {
            console.log("送信成功:", data);
            resolve();
          } else {
            console.log("送信失敗");
            reject(new Error("送信に失敗しました"));
          }
        }, 1500);
      });
    } catch (error) {
      console.error("エラー:", error);
      setError("root.serverError", {
        type: "manual",
        message: "送信に失敗しました。もう一度お試しください。",
      });
    }
  };

  const handleReset = () => {
    reset();
  };

  useEffect(() => {
    const handleBeforeUnload = (e: BeforeUnloadEvent) => {
      if (isDirty) {
        e.preventDefault();
      }
    };

    window.addEventListener("beforeunload", handleBeforeUnload);

    return () => {
      window.removeEventListener("beforeunload", handleBeforeUnload);
    };
  }, [isDirty]);

  useEffect(() => {
    console.table([
      {
        状態: "フォーム状態",
        isDirty,
        isValid,
        isSubmitting,
        isSubmitted,
        isSubmitSuccessful,
      },
    ]);

    console.table(
      Object.entries(errors).map(([key, value]) => ({
        フィールド: key,
        エラーメッセージ:
          value && typeof value === "object" && "message" in value
            ? value.message
            : JSON.stringify(value),
      }))
    );
  }, [errors, isDirty, isSubmitSuccessful, isSubmitted, isSubmitting, isValid]);

  return (
    <Container>
      <Typography variant="body2" sx={{ textAlign: "right", mb: 4 }}>
        [ MUI ]
      </Typography>
      <Box
        component="form"
        onSubmit={handleSubmit(onSubmit)}
        noValidate
        sx={{ mt: 1 }}
      >
        {isSubmitting && (
          <Box
            sx={{
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              my: 2,
            }}
          >
            <CircularProgress size={40} sx={{ mr: 2 }} />
            <Typography variant="body1">送信中...</Typography>
          </Box>
        )}
        {!isSubmitting && isSubmitted && (
          <Alert
            severity={isSubmitSuccessful ? "success" : "error"}
            sx={{ alignItems: "center" }}
          >
            {isSubmitSuccessful ? "送信成功" : "送信失敗"}
            {errors.root?.serverError && (
              <Button onClick={() => handleSubmit(onSubmit)()}>リトライ</Button>
            )}
          </Alert>
        )}
        <Controller
          name="fullName"
          control={control}
          rules={{ required: "名前が入力されてないようです。" }}
          render={({ field, fieldState: { error } }) => (
            <Box>
              <TextField
                {...field}
                margin="normal"
                required
                fullWidth
                id="fullName"
                label="名前"
                error={!!error}
                helperText={error?.message}
              />
            </Box>
          )}
        />
        <Controller
          name="gender"
          control={control}
          rules={{ required: "性別が選択されてないようです。" }}
          render={({ field, fieldState: { error } }) => (
            <Box>
              <FormControl fullWidth margin="normal" error={!!error}>
                <InputLabel id="gender-label">性別</InputLabel>
                <Select
                  {...field}
                  labelId="gender-label"
                  id="gender"
                  label="性別"
                >
                  <MenuItem value={Gender.empty}>
                    <em>選択してください</em>
                  </MenuItem>
                  <MenuItem value={Gender.female}>女性</MenuItem>
                  <MenuItem value={Gender.male}>男性</MenuItem>
                  <MenuItem value={Gender.other}>無回答</MenuItem>
                </Select>
                {error && (
                  <Typography color="error">{error.message}</Typography>
                )}
              </FormControl>
            </Box>
          )}
        />
        <Controller
          name="comment"
          control={control}
          rules={{
            minLength: { value: 3, message: "3文字以上で入力してください" },
            maxLength: { value: 10, message: "10文字以下で入力してください" },
          }}
          render={({ field, fieldState: { error } }) => (
            <Box>
              <TextField
                {...field}
                margin="normal"
                fullWidth
                id="comment"
                label="コメント"
                multiline
                rows={4}
                error={!!error}
                helperText={error?.message}
              />
            </Box>
          )}
        />
        <Controller
          name="newsLatter"
          control={control}
          rules={{ required: "お知らせの受け取りについて選択してください" }}
          render={({ field, fieldState: { error } }) => (
            <Box>
              <FormControl component="fieldset" margin="normal" error={!!error}>
                <Typography component="legend">お知らせ</Typography>
                <RadioGroup {...field} row>
                  <FormControlLabel
                    value={NewsLatter.receive}
                    control={<Radio />}
                    label="受け取る"
                  />
                  <FormControlLabel
                    value={NewsLatter.reject}
                    control={<Radio />}
                    label="受け取らない"
                  />
                </RadioGroup>
                {error && (
                  <Typography color="error">{error.message}</Typography>
                )}
              </FormControl>
            </Box>
          )}
        />
        <Controller
          name="agree"
          control={control}
          rules={{ required: "チェックボックスをチェックしてください" }}
          render={({ field, fieldState: { error } }) => (
            <FormControlLabel
              control={
                <Checkbox {...field} checked={field.value} color="primary" />
              }
              label="利用規約に同意する"
            />
          )}
        />
        {errors.agree && (
          <Typography color="error">{errors.agree.message}</Typography>
        )}
        <Box sx={{ mt: 10, display: "flex", justifyContent: "flex-end" }}>
          <Button
            type="submit"
            variant="contained"
            color="primary"
            disabled={!isDirty || !isValid}
            sx={{ mr: 2 }}
          >
            送信
          </Button>
          <Button type="button" variant="outlined" onClick={handleReset}>
            リセット
          </Button>
        </Box>
      </Box>
    </Container>
  );
}

素の HTML の時と同じように Controller コンポーネントの render 部分に MUI のコンポーネントを組み込んでいます。

このように他のライブラリと react-hook-form を組み合わせる時には Controller コンポーネントを利用するようことになります。
そのため {...register} は使用せずに Controller コンポーネントを使用しするやり方に統一しておいた方が、修正範囲も少なくなるので個人的には好みです。

以上で react-hook-form の使い方の紹介を終わります。
最後までお読みいただき、ありがとうございました。この記事が皆様のお役に立てば幸いです。

Discussion

koji-kojikoji-koji

タイトルと本文のタイポが少し気になったので共有させていただきます。

誤り) raect-hook-form
正解) react-hook-form