🫧

React Hook Formで簡単なフォームを作成してみる

2023/09/29に公開

こんにちは!
スペースマーケットでフロントエンドエンジニアをしているwharaguchiです。

今回は、React Hook Formについての記事になります。

対象読者

  • React Hook Formを軽く触ってみたい方
  • React Hook Formでどんなことができるか知りたい方

React Hook Form is 何

React Hook FormはReactでフォームを簡単に扱うことのできるライブラリです。
ReactのHooksを使用して、フォームの管理状態やバリデーションを行うことができます。

https://react-hook-form.com/

やること

useStateを使用して作成したフォームをReact Hook Formで書き換えてみたいと思います。

フォームの項目と仕様は以下の通りです。

    • 必須項目
    • 必須項目
  • コメント
    • 必須項目
    • 10文字以上の入力が必要

useStateを使用したフォーム

まずはReactのuseStateを使ってフォームを書いてみます。

CodeSandboxのリンクを貼っておくので、実際に動かして見たいという方はこちらもどうそ。
https://codesandbox.io/s/react-form-sample-xzv5mm?file=/src/App.tsx

import { FC, ChangeEvent, useState } from "react";
import "./styles.css";

const App: FC = () => {
  // 各項目の入力情報
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [comment, setComment] = useState("");

  // 各項目のエラーメッセージ
  const [firstNameErrorMessage, setFirstNameErrorMessage] = useState("");
  const [lastNameErrorMessage, setLastNameErrorMessage] = useState("");
  const [commentErrorMessage, setCommentErrorMessage] = useState("");

  // 送信処理
  const onSubmit = (e) => {
    e.preventDefault();
    // エラーメッセージの初期化
    setFirstNameErrorMessage("");
    setLastNameErrorMessage("");
    setCommentErrorMessage("");

    const formData = {
      firstName,
      lastName,
      comment,
    };

    // エラーのチェック
    const emptyFirstName = firstName === "";
    const emptyLastName = lastName === "";
    const insufficientCommnetLength = comment.length < 10;
    if (emptyFirstName) setFirstNameErrorMessage("名を入力してください");
    if (emptyLastName) setLastNameErrorMessage("姓を入力してください");
    if (insufficientCommnetLength)
      setCommentErrorMessage("コメントは10文字以上で入力してください");
    const enableSubmit =
      !emptyFirstName && !emptyLastName && !insufficientCommnetLength;

    if (enableSubmit) {
      console.log("submit!!");
      console.log(formData);
    }
  };
  // onChange
  const onChangeFirstName = (e: ChangeEvent<HTMLInputElement>) => {
    setFirstName(e.target.value);
  };
  const onChangeLastName = (e: ChangeEvent<HTMLInputElement>) => {
    setLastName(e.target.value);
  };
  const onChangeComment = (e: ChangeEvent<HTMLTextAreaElement>) => {
    setComment(e.target.value);
  };

  return (
    <div className="wrapper">
      <h1>React Form</h1>
      <section className="section">
        <h2>useState Form</h2>
        <p>ReactのuseStateを使用してformを作成した例です。</p>
        <form onSubmit={onSubmit}>
          <div className="form-item">
            <label>
              <span className="label required">必須</span>
              <span></span>
              <input
                type="text"
                name="lastName"
                value={lastName}
                onChange={onChangeLastName}
              />
            </label>
            {lastNameErrorMessage && (
              <p className="error-message">{lastNameErrorMessage}</p>
            )}
          </div>
          <div className="form-item">
            <label>
              <span className="label required">必須</span>
              <span></span>
              <input
                type="text"
                name="firstName"
                value={firstName}
                onChange={onChangeFirstName}
              />
            </label>
            {firstNameErrorMessage && (
              <p className="error-message">{firstNameErrorMessage}</p>
            )}
          </div>
          <div className="form-item">
            <label>
              <span className="label required">必須</span>
              <span>コメント</span>
              <textarea
                onChange={onChangeComment}
                value={comment}
                name="comment"
              />
            </label>
            {commentErrorMessage && (
              <p className="error-message">{commentErrorMessage}</p>
            )}
          </div>
          <div className="submit-button">
            <input type="submit" />
          </div>
        </form>
      </section>
      <p>姓: {lastName}</p>
    </div>
  );
};

シンプルなサンプルコードになりますが、onChangeイベントでuseStateの値を更新し、submitのタイミングで入力項目のバリデーションを行っています。

そして入力項目に不足があれば、エラーメッセージを管理するuseStateの値を更新し、コンポーネント上でもエラーメッセージを表示するようにしています。

この規模のフォームであればまだいいですが、もっと大きな規模のフォームやバリデーションの項目が増えた場合は、ちょっとつらみが出てきそうです。

React Hook Formを使用したフォーム

これをReact Hook Formで書くと以下のようになります。

こちらもCodeSandboxのリンクを貼っておきます。
https://codesandbox.io/s/react-hook-form-sample-w24c6l?file=/src/App.tsx

import { FC } from "react";
import "./styles.css";

import { useForm, SubmitHandler } from "react-hook-form";

type Inputs = {
  firstName: string;
  lastName: string;
  comment: string;
  submit: any;
};

const App: FC = () => {
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors },
  } = useForm<Inputs>();
  const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);

  // watch
  const lastName = watch("lastName");

  return (
    <div className="wrapper">
      <h1>React Form</h1>
      <section className="section">
        <h2>useState Form</h2>
        <p>React Hook Formを使用してformを作成した例です。</p>
        <form onSubmit={handleSubmit(onSubmit)}>
          <div className="form-item">
            <label>
              <span className="label required">必須</span>
              <span></span>
              <input
                type="text"
                {...register("lastName", {
                  required: "姓を入力してください",
                })}
              />
            </label>
            {errors.lastName?.message && (
              <p className="error-message">{errors.lastName?.message}</p>
            )}
          </div>
          <div className="form-item">
            <label>
              <span className="label required">必須</span>
              <span></span>
              <input
                type="text"
                {...register("firstName", {
                  required: "名を入力してください",
                })}
              />
            </label>
            {errors.firstName?.message && (
              <p className="error-message">{errors.firstName?.message}</p>
            )}
          </div>
          <div className="form-item">
            <label>
              <span className="label required">必須</span>
              <span>コメント</span>
              <textarea
                {...register("comment", {
                  required: true,
                  minLength: {
                    value: 10,
                    message: "10文字以上で入力してください",
                  },
                  maxLength: {
                    value: 20,
                    message: "20文字以下で入力してください",
                  },
                })}
              />
            </label>
            {errors.comment?.message && (
              <p className="error-message">{errors.comment.message}</p>
            )}
          </div>
          <div className="submit-button">
            <input type="submit" />
          </div>
        </form>
      </section>
      <p>姓: {lastName}</p>
    </div>
  );
};

HTMLの構成はuseStateとほぼ変わりませんが、ロジック部分のコード量が大分減りスッキリしました。

以下解説です。

useForm

https://react-hook-form.com/docs/useform

useFormは、React Hook Formのライブラリ内で提供されるカスタムフックです。
フォームの状態管理やバリデーションルールを設定するために使用します。

register

register関数は、使用することでバリデーションルールを追加することができます。
このルールの追加が直感的というかかなりわかりやすくて良い点だと個人的に思っています。

ちなみに公式ドキュメントでは以下のように書かれていますが、サンプルコードで書いている通り、textareaタグでも普通に使えそうです。

This method allows you to register an input or select element and apply validation rules to React Hook Form.
訳(DeepL)このメソッドでは、input または select 要素を登録し、React Hook Form にバリデーションルールを適用します。

以下にいくつか例を記載します。

必須項目の指定

<input
  {...register("firstName", {
    required: true,
  })}
/>;

requiredの値をbooleanではなく文字列にすることで、このあとに説明するformStateerrorsオブジェクトにここで指定したエラーの文字列が返ってくるようになります。とても便利ですね

<input
  {...register("firstName", {
    required: "名を入力してください",
  })}
/>;

文字数指定

文字数の指定もできます。
最低文字数を指定したい場合は、minLengthで、最大文字数を指定したい場合はmaxLengthで指定することができます。

<textarea
  {...register("comment", {
    minLength: 10, // 最低文字数
    maxLength: 20, // 最大文字数
  })}
/>;

また以下のように書くことで条件に達していない場合のエラー文を指定することもできます。

<textarea
  {...register("comment", {
    minLength: {
      value: 10,
      message: "10文字以上で入力してください",
    },
    maxLength: {
      value: 20,
      message: "20文字以下で入力してください",
    },
  })}
/>;

入力した文字数がバリデーションに引っかかった場合は、上述しているformState.errorsに以下のような形でエラーが返ってきます。

errors: {
  // registerの第一引数で指定したname
  comment: {
    type: "minLength";
    message: "10文字以上で入力してください";
    ref: <textarea name="comment"></textarea>;
  }
}

そのため上記のように文字列が不足しているエラーが発生している場合には、errors.comment.messageの値をエラーの表示として画面に出力することができます。

{
  errors.comment?.message && (
    <p className="error-message">{errors.comment.message}</p>
  );
}

他にも様々なoptionがあるので気になる方は公式ドキュメントをご確認ください。
https://react-hook-form.com/docs/useform/register

handleSubmit

https://react-hook-form.com/docs/useform/handlesubmit

handleSubmit関数は、フォームが送信されるときに実行される関数を指定することができます。

バリデーションルールにエラーがある場合はフォームの送信がされず、エラーがない場合にのみhandleSubmit関数に指定した関数が実行されるようになっています。

// バリデーションルールにエラーがなければonSubmit関数が実行される
<form onSubmit={handleSubmit(onSubmit)}>

watch

watch関数はその名の通り特定のフォームの入力フィールドの値を監視することができます。

監視されているフィールドの値をリアルタイムで取得することができたり、その値に基づいて他のアクションを実行することができます。

サンプルコードでは、姓の入力フィールドを監視しており、姓の入力フィールドに入力した値をリアルタイムでコンポーネント上に表示しています。

const lastName = watch("lastName");

https://react-hook-form.com/docs/useform/watch

formState

formStateオブジェクトは、フォームの状態の情報を保持しています。

以下にいくつか例を示します。

key 説明
errors オブジェクト バリデーションルールのエラー情報を保持しているオブジェクト
isDirty boolean フォーム内のフィールドが変更されたか
isValid boolean フォーム内のすべてのフィールドがバリデーションをパスしたか
isSubmitting boolean フォームが送信中の場合はtrueを返す。それ以外はfalse

サンプルコードではここまで使いこなせていませんが、isValidで送信ボタンのdisabledの制御を行ったり、isDirtyでフォームを変更した際にアラートを表示したり、様々な使い方ができそうです。

こちらも公式ドキュメントでより詳しい解説がされているので、気になる方はご確認ください。

https://react-hook-form.com/docs/useform/formstate

最後に

いかがでしたでしょうか?

サンプルコードのような小さい規模のフォームでもここまで記述量が減りますし、マークアップ、フォームの入力ロジック、バリデーションのロジックをひとまとめに書くことができることで書きやすく可読性も上がります。

もっと規模の大きいフォームを実装する際はより大きな効果を得られると思います。

今回は以上となります!
最後まで読んでいただきありがとうございました!

GitHubで編集を提案
スペースマーケット Engineer Blog

Discussion