🐶

React Hook Form ( TypeScript ) で投稿フォームを作る

2023/08/28に公開

せっかくReactを使うからにはモジュールを利用して簡単に複雑な処理を行いたい。
Formの作成をReact Hook Formで行う。
まずは基本的なフォームの機能を整えてみる。
使用するReact Hook FormはV7です。V6までや、V8以降が出たときは動作が変わっている可能性があるので注意してください。

自分用のメモ書きなので、間違いやより良い方法があればご指摘ください。

React Hook Formのインストール

ターミナル内でnpmを使ってインストールする

npm i react-hook-form
npm i @hookform/error-message

react-hook-formだけでもエラーメッセージの表示まで出来るのですが、公式ではerror-messageを使用するよう勧めていたのでこちらもインストールしておきます。

React Hook Formを使った例

create-react-appのデフォルトのApp.tsxを編集して動作を確認していきます。
コード全体を

App.tsx
import { useForm } from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message';

// Formに入力したデータ保存用のInterface
interface InputData {
  username: string;
  gender: string;
  month: string;
  date: string;
  message: string;
}

function App() {
  // 月リスト
  const monthList: number[] = [];
  for(let i = 1; i <= 12; i++){
    monthList.push(i);
  }

  // 日リスト
  const dateList: number[] = [];
  for(let i = 1; i <= 31; i++){
    dateList.push(i);
  }

  // 月の日にち最大値リスト
  let datesOfMonth= [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

  // useForm関数の設定
  const {
    register,
    handleSubmit,
    getValues,
    formState: { errors },
  } = useForm<InputData>({ mode: 'onChange', defaultValues: { gender: 'female' } });

  // Submitイベント
  const onSubmit = (data: InputData) => {
    alert(`Name: ${data.username}\nGender: ${data.gender}\nBirthday: ${data.month}/${data.date}\nMessage: ${data.message}\n`);
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h2>Input Form Test</h2>
      <div>
        <div>名前</div>
        <div>
          <input type="text"
            {...register("username", {
              required: "名前を入力してください",
            })} />
        </div>
        <ErrorMessage
          errors={errors}
          name="username"
          render={({ message }) => <div>{message}</div>}
        />
      </div>

      <div>
        <div>性別</div>
        <div>
          <input type="radio" id="gender-male" value="male"
            {...register("gender", {
              required: true,
            })} />
          <label htmlFor="gender-male"></label>
          <input type="radio" id="gender-female" value="female"
            {...register("gender", {
              required: true,
            })} />
          <label htmlFor="gender-female"></label>
        </div>
      </div>

      <div>
        <div>誕生日</div>
        <div>
        <select
            {...register("month")}>
            {monthList.map((item) => {
              return (
                <option key={item} value={item}>
                  {item}</option>
              );
            })}
          </select>
          <select
            {...register("date", {
              validate: {
                message: value =>{
                  const month = Number(getValues('month'));
                  const date = Number(value);
                  if(datesOfMonth[month - 1] < date){
                    return "正しい日付を入力してください";
                  }
                  return;
                }
              }
            })}>
            {dateList.map((item) => {
              return (
                <option key={item} value={item}>
                  {item}</option>
              );
            })}
          </select>
        </div>
        <ErrorMessage
          errors={errors}
          name="date"
          render={({ message }) => <div>{message}</div>}
        />
      </div>

      <div>
        <div>その他(100文字以内)</div>
        <div>
          <textarea
            {...register("message", {
              maxLength: { value: 100, message: "100文字以内で入力してください" }
            })}
          ></textarea>
        </div>
        <ErrorMessage
          errors={errors}
          name="message"
          render={({ message }) => <div>{message}</div>}
        />
      </div>

      <button type="submit">送信</button>
    </form>
  );
}

export default App;

実際に実行した画面がこちらです。CSSって大事。

React Hook Formの使い方

基本的な使い方ですが、一度React Hook FormのページとAPIマニュアルだけは確認しておきましょう。
読まずとも、どんな機能があるのかさらっと読んでおくと後からの理解が違います。
また、どの記事を読んでいても必ずコピペミスなど間違いはあるので、最後に頼れるのはマニュアルです。
https://www.react-hook-form.com/

インポートとデータ型設定

import { useForm } from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message';

// Formに入力したデータ保存用のInterface
interface InputData {
  username: string;
  gender: string;
  month: string;
  date: string;
  message: string;
}

react-hook-formからuseFormをインポートします。
useFormはフォームを簡単に管理するためのカスタムフックです。
この後、useFormの中でも利用したい機能を、関数コンポーネントの中で宣言します。
エラーメッセージの表示のためにErrorMessageもインポートしています。

また、フォームで送信したいデータの型を宣言しておきます。
今回のフォームでは、名前・性別・星座・その他の4つのデータを入力出来るようにします。

useFormの設定

  // useForm関数の設定
  const {
    register,
    handleSubmit,
    getValues,
    formState: { errors },
  } = useForm<InputData>({ mode: 'onChange', defaultValues: { gender: 'female' } });

useForm関数の設定を行います。
register, handleSubmit, getValues, formStateのうちerrorsの機能を使用します。
また、フォームの管理するデータ型を先ほど作成したInputData型に指定しています。

register

フォームに入力された内容を検証、登録を行う関数です。

handleSubmit

送信ボタンを押して、検証が成功した場合にフォームデータを受けるイベントです。

getValues

フォームで設定された値を読み取るための関数。

formState: { error }

formStateはフォーム全体の状態を把握するのに使用できます。
このうちのerrorオブジェクトを使い、エラーメッセージの取得を行います。

useForm

useFormの型として先ほど作成したInputDataのインターフェースを指定します。
設定できるpropsもいくつかありますが、modeとdefaultValuesのみ指定します。

mode

イベントのトリガーを指定します。onChange, onBlur, onSubmit, onTouchedなどが指定可能です。今回はonChangeを指定し、フォームの内容が書き換わったときにuseFormの中身を変更していきます。

defaultValues

defaultValueはフォームデータの初期値を変更します。このあと、ラジオボタンのところで説明しますが、genderのみ'male'を初期値として設定しておきます。

onSubmitイベント

// Submitイベント
const onSubmit = (data: InputData) => {
  alert(`Name: ${data.username}\nGender: ${data.gender}\nSign: ${data.sign}\nMessage: ${data.message}\n`);
}

return (
  <form onSubmit={handleSubmit(onSubmit)}>
  ...

送信ボタンを押した時に実行する関数です。フォームに入力された内容をdata変数で受け取り、アラーム表示します。

この関数を呼び出しているのが、formタグのonSubmitイベントです。
ここで先ほどのuseFormで宣言したhandleSubmitを使用しています。
送信ボタンを押すと、最初にhandleSubmitが呼び出されフォームの入力内容の検証を行います。正常であればInputData型の入力内容データをonSubmit関数へ渡して実行します。
エラーがあれば設定されたメッセージを表示し、エラーのあったコントロールにフォーカスを合わせます。

Input(Type: text)

<div>
  <input type="text"
    {...register("username", {
      required: "名前を入力してください",
    })} />
</div>
<ErrorMessage
  errors={errors}
  name="username"
  render={({ message }) => <div>{message}</div>}
/>

フォームの名前入力部分です。
通常のHTLMコードとして属性入力しているのはtypeのみです。ここでregisterの機能を使います。

registerの第一引数として、このinputボックスに対応するInputDataの変数を指定します。
useFormの宣言時にmodeにonChangeを指定しているため、このコントロールの内容が書き換わると自動的にregisterで指定した"username"の中身が書き換わります。

第二引数で検証に関する設定を行っています。今回はrequiredを指定しています。何も入力されていないときに検証失敗となり、メッセージを返します。エラーメッセージまでいらないが、内容があるかどうかの検証だけを行いたい場合はtrueを指定します。

ErrorMessageタグでここにエラーメッセージを埋め込みます。
usernameのデータに問題があった場合はrenderの内容をHTMLに埋め込みます。
公式ではエラーの表示方法はこれしか見つけれなかったのですが、Reactコンポーネントを使って下記の方法でもエラーメッセージを表示することができます。

<div>
  {errors.username?.message as React.ReactNode}
</div>

ErrorMessageタグの方がメッセージテキストだけではなく、タグの埋め込みだけも可能なために汎用性が高そうですね。

Input(Type: radio)

<div>
  <input type="radio" id="gender-male" value="male"
    {...register("gender", {
      required: true,
    })} />
  <label htmlFor="gender-male"></label>
  <input type="radio" id="gender-female" value="female"
    {...register("gender", {
      required: true,
    })} />
  <label htmlFor="gender-female"></label>
</div>

ラジオボックスの処理です。
チェックをつけると、valueで指定した値をregisterの第一引数に指定した"gender"に代入します。
見てお気づきの通り、HTMLではcheckedの指定をしていないため、どちらもチェックがついていない状態でページが表示されることになります。
しかし、userFormのところでdefaultValuesに"male"を指定していることで、valueに"male"を指定している「男」のラジオボタンにチェックがついた状態で表示されます。
defaultValueのところを"female"に書き換えることで初期状態が女性になり、defaultValueを消すことでどちらもチェックがつかないことを確認してみましょう。

Select

<div>
  <select
    {...register("month")}>
    {monthList.map((item) => {
      return (
	<option key={item} value={item}>
	  {item}</option>
      );
    })}
  </select>
  <select
    {...register("date", {
      validate: {
	message: value => {
	  const month = Number(getValues('month'));
	  const date = Number(value);
	  if (datesOfMonth[month - 1] < date) {
	    return "正しい日付を入力してください";
	  }
	  return;
	}
      }
    })}>
    {dateList.map((item) => {
      return (
	<option key={item} value={item}>
	  {item}</option>
      );
    })}
  </select>
</div>
<ErrorMessage
  errors={errors}
  name="date"
  render={({ message }) => <div>{message}</div>}
/>

Selectの場合の表記です。月日を選び、ありえない日付を選んだ時にエラーを吐き出します。
validateを使うことで複雑な条件での検証を行うことができます。messageが必要ない場合はもう少し簡素に書くこともできます。
日にちの検証のために現在の月の入力値をgetValuesを使って取得します。

getValues('month')

これで現在のフォームのmonthに割り当てられた値を読み出します。

Textarea

<div>
  <textarea
    {...register("message", {
      maxLength: { value: 100, message: "100文字以内で入力してください" }
    })}
  ></textarea>
</div>
<ErrorMessage
  errors={errors}
  name="message"
  render={({ message }) => <div>{message}</div>}
/>

registerの検証としてmaxLengthを使用しています。100文字を超えるとエラーとなります。
逆にmixLengthを使うと最低文字数を指定することもできます。

Submit button

<button type="submit">送信</button>

こちらは通常のフォームと変わりありません。
押すことでFormのonSubmitが発行され、フォーム内の検証が行われます。

まとめ

以上が簡単なReact Hook Formの使い方です。
プレーンな状態で行うと、すべてのコントロール用にonChangeイベントの関数を作り、onChangeイベントをつけて・・・なんてやっていたのがかなりシンプルになったかと思います。

Discussion