🔲

【React】React Hook Formで入力フォームをつくる

2024/11/07に公開

Webサイトでよく見るかける問い合わせフォームをReactで実装しました。以下では忘備録としてその内容についてまとめています。(※サンプルのためごく簡素なスタイルとしています)

内容としては前半としてReact Hook Formを使ったフォームの実装について、後半部分でEmailJSというツールを使ってフォームの入力内容をメール送信する機能の実装について書いていきたいと思います。

使用環境

  • Typescript: 4.9.5
  • React: 18.2.0
  • React Hook Form: 7.50.1

React Hook Formで入力フォームを作る

今回、フォームを作るにあたり、React Hook Formというライブラリを使用しました。
改めてReact Hook Formとはなんぞやと思ってPerplexityに聞いてみるとこんな感じでした。

React Hook FormはReactアプリケーションでフォームの状態管理を簡素化するバリデーションライブラリです。フォームのバリデーションやエラーハンドリングが簡単に行えるAPIを提供しており、開発者は直感的に使うことができます。さらにカスタムコンポーネントとも簡単に統合できます。

この最後の一文に書いている、カスタムコンポーネントとも簡単に統合できるというのは作成時には知らなかったことだったので、この部分についても後ほど詳しく確認できればと思います。

React-Hook-Formのインストール

ではまずReact Hook Formを以下コマンドでプロジェクトにインストールします。

npm install react-hook-form

インストール後は以下のようにuseFormをインポートして使用します。

import { useForm } from 'react-hook-form'

useFormを呼び出すことでregister関数、handleSubmit関数、reset関数、errorsオブジェクトなど、フォーム管理に必要な関数やオブジェクトを取得することができます。

以下はTestFormコンポーネントの中でuseFromを初期化する例です。
なお、フォームの実装部分については、現段階では冒頭キャプチャ通りの見た目を実装しただけの状態です。

import { useForm } from 'react-hook-form'

const TestForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();
	
	reuturn (
    <form style={{ margin: "20px 40px", width: "100%" }}>
      <label htmlFor='name'>お名前</label><br />
      <input type='text' style={{ width: "20%" }} /><br />

      <label htmlFor='furigana'>フリガナ</label><br />
      <input type='text' style={{ width: "20%" }} /><br />

      <label htmlFor='email'>メールアドレス</label><br />
      <input type='email' style={{ width: "20%" }} /><br />

      <label htmlFor='content'>お問い合わせ内容</label><br />
      <textarea style={{ width: "20%" }} /><br />

	    <input type='checkbox' />
      <label><span style={{ marginLeft: "10px"}}> 利用規約に同意しました</span></label><br />
      
      <input type="submit" value="送信する" />
    </form>
	)
}

export default TestForm

useFormの機能を利用する

このフォームではお名前、フリガナ、メールアドレス、お問い合わせ内容、同意のチェックボックスの5つの入力フィールドがあります。
まずはそれぞれのフィールドに対応する型を定義しておきます。

こうすることで、register, handleSubmit, errors などの useForm から提供されるメソッドやプロパティが、InputTypes に沿った入力データを扱うようになり、フォームの各フィールド(company, name, email, content, consent)が指定した型に一致しない場合はエラーを表示してくれるようになり、型安全性を保証することができます。

import { useForm } from 'react-hook-form'

type InputTypes = {
  name: string;
  furigana: string;
  email: string;
  content: string;
  accepted: boolean;
}

const TestForm = () => {
	const { register, handleSubmit, formState: { errors } } = useForm<InputTypes>();
	
 // ...

register関数

register関数はフォームのフィールド(input要素、select要素など)をReact Hook Formの管理下におくための関数です。
以下のようにスプレッド構文を使って、各フィールドに適用していきます。

<input ...register("フィールド名") />

これにより、指定したフィールドがフォームの一部として認識され、フォーム送信時にその値が取得されるようになります。
register関数の引数にはフィールド名とオプションの2種類があります。

  • フィールド名(必須): フォーム送信時にアクセスできるオブジェクトのキーとして使用されます。このフィールド名はこのフォームが送信するデータオブジェクトのプロパティ名として使われるため、先ほどInputTypes 型で定義したInputTypesのキー名と一致させる必要があります。
  • バリデーションオプション: required, pattern などを指定して個々のフィールドに対して任意のバリデーションルールを設定することができます。

以下は、お名前とフリガナのフィールドに必須入力のバリデーションを加えた例です。

import { useForm } from 'react-hook-form'

type InputTypes = {
  name: string;
  furigana: string;
  email: string;
  content: string;
  consent: boolean;
}

const TestForm = () => {
	const { register, handleSubmit, formState: { errors } } = useForm<InputTypes>();
	
	reuturn (
    <form style={{ margin: "20px 40px", width: "100%" }}>
      <label htmlFor='name'>お名前</label><br />
      <input 
	      type='text'
		    style={{ width: "20%" }}
		    {...register("name", { required: '入力が必須の項目です' })}
		  /><br />

      <label htmlFor='furigana'>フリガナ</label><br />
      <input
	      type='text'
	      style={{ width: "20%" }}
		    {...register("furigana", { required: '入力が必須の項目です' })}
	    /><br />
	    
	    {// ... }
   
    </form>
	)
}

export default TestForm

ただし、このままの状態では未入力の状態で送信ボタンをクリックする時にバリデーションが動作しているようには見えません。バリデーションエラーが発生していることを視覚的に表示するためにはuseFormの提供するerrorsオブジェクトを参照し、どの要素にバリデーションが起こっているかを検知する必要があります。

errorsオブジェクトとバリデーションエラーのタイミング

errorsオブジェクトはregisterで管理した各フィールドでバリデーションエラーが発生した場合にエラー情報が格納されるオブジェクトです。errors は、{ [registerで設定したフィールド名]: エラー情報 } の形でプロパティを持ち、各フィールド名に対応するエラー情報が含まれます。なお、エラーが発生しなかったフィールドのフィールド名はerrorsオブジェクト内には追加されません。

そのため、入力フィールドの外枠を赤色にする、もしくは入力フィールドの付近にエラーメッセージを表示するといったバリデーションエラーを視覚的に表現するためには、errorsオブジェクト内に対応するフィールド名が存在するかどうかをフラグとし、さらにそのerrorsオブジェクトから、フィールド名に対応するエラー情報を取得して表示するという流れになります。

errorsオブジェクトの持つエラー情報についてもう少し詳しく確認します。
例えばnameフィールドでバリデーションエラーが発生した場合、errors.name以下には次のような情報が格納されます。

  • type: エラーの種類を表します。requiredpatternminLengthmaxLength など、registerで設定した、どのバリデーションルールに違反したかを示します。
    今回はrequiredを設定しているので、errors.name.type には "required" が格納されます。なお、registerで複数のバリデーションルールを設定している場合、typeには最初に違反したバリデーションルールだけが格納されます。
  • message: registerで設定したエラーメッセージが格納されます。
    今回のnameフィールドでバリデーションエラーが発生した場合はerrors.name.messageには「入力が必須の項目です」というエラーメッセージが格納されます。
  • ref: エラーが発生したフォームフィールドの参照(ref)を指しています。
    例えば、errors.name.refname フィールドの参照を取得でき、エラーがあるフィールドにフォーカスを移すなどの操作に利用できます。

また、errorsオブジェクトが更新されるタイミング(バリデーションエラーが発生するタイミング)は、デフォルトの設定ではuseFormから提供されるhandleSubmit関数が呼ばれた時なのですが、React Hook Formではこのタイミングを、フィールドからフォーカスが外れたときや、フィールドの内容が変わったときなど、バリデーションエラーがリアルタイムで発生するような形に制御することも可能です。
制御するにはuseFormの初期化時に、modeオプションを渡し、そこにバリデーションエラーを発生させるタイミングを指定します。

const { register, handleSubmit, formState: { errors } } = useForm<InputTypes>({
	mode: "onBlur"  // "onChange" や "onSubmit" なども指定可能
});

modeに渡すことができる値としては以下の通りです。

  • onBlur: フォーカスが外れたときにバリデーションを行う
  • onChange: 値が変更されるたびにバリデーションを行う
  • onSubmit: handleSubmit 発火時のみバリデーションを行う
  • all: すべてのイベントでバリデーションを行う(リアルタイム更新)

実装例

今回はデフォルト設定のまま、送信ボタンがクリックされたタイミングでバリデーションエラーを発生させることとします。
先ほどのサンプルコードのフォーム部分にerrorsオブジェクトと、handleSubmit関数を追加していきます。handleSubmitのコールバックとしては、とりあえずフォームに入力されたデータを出力するようにしています。

import { useForm } from 'react-hook-form'

type InputTypes = {
  name: string;
  furigana: string;
  email: string;
  content: string;
  consent: boolean;
}

const TestForm = () => {
	const { register, handleSubmit, reset, formState: { errors } } = useForm<InputTypes>();
	
	const onSubmit = (data: InputTypes) => {
		console.log(data)
	}
	
	reuturn (
    <form onSubmit={handleSubmit(onsubmit)} style={{ margin: "20px 40px", width: "100%" }}>
      <label htmlFor='name'>お名前</label><br />
      {errors.name && ( <><span style={{ color: "red" }}>{errors.name.message}</span><br /></> )}
      <input type='text' id='name'
        style={{ width: "20%", borderColor: errors.name ? "red" : "inherited" }}
        {...register("name", { required: '入力が必須の項目です' })}
      /><br />
      
      <label htmlFor='furigana'>フリガナ</label><br />
      {errors.furigana && ( <><span style={{ color: "red" }}>{errors.furigana.message}</span><br /></> )}
      <input type='text' id='furigana'
        style={{ width: "20%", borderColor: errors.furigana ? "red" : "inherited" }}
        {...register("furigana", { required: '入力が必須の項目です' })}
      /><br />
      	    
	    {// ... }
   
    </form>
	)
}

export default TestForm

こうすると、nameフィールドとfuriganaフィールドに入力必須のバリデーションが適用され、エラー発生時は入力フォームの側に設定したエラーメッセージを表示させることができました。

他にも、errorsオブジェクトの有無でinput要素に付与するCSSクラス名を切り替えることができれば、エラー時に入力フォームの枠線の色を変えるといった動きも実装できそうです。(上記キャプチャで黄色に変わっているのはHTMLの仕様のはず…?)

ちなみに、それぞれのフォームに値を入力して送信ボタンを押してみると、以下のように、dataオブジェクトから各フィールドに入力された値が確認できました。

では続いて、emailフィールド、contentフィールド、acceptedフィールドにバリデーションを適用していきます。

return (
    <form onSubmit={handleSubmit(onsubmit)} style={{ margin: "20px 40px", width: "100%" }}>
      <label htmlFor='name'>お名前</label><br />
      {errors.name && ( <><span style={{ color: "red" }}>{errors.name.message}</span><br /></> )}
      <input type='text' id='name'
        style={{ width: "20%" }}
        {...register("name", { required: '入力が必須の項目です' })}
      /><br />

      <label htmlFor='furigana'>フリガナ</label><br />
      {errors.furigana && ( <><span style={{ color: "red" }}>{errors.furigana.message}</span><br /></> )}
      <input type='text' id='furigana'
        style={{ width: "20%" }}
        {...register("furigana", { required: '入力が必須の項目です' })}
      /><br />

      <label htmlFor='email'>メールアドレス</label><br />
      {errors.email && ( <><span style={{ color: "red" }}>{errors.email.message}</span><br /></> )}
      <input type='email' id='email'
        style={{ width: "20%" }}
        {...register(
          "email",
          {
            required: '入力が必須の項目です',
            pattern: {
              value: /[\w\-._]+@[\w\-._]+\.[A-Za-z]+/,
              message: 'メールアドレスの形式が正しくありません'
            }
          }
        )}
      /><br />

      <label htmlFor='content'>お問い合わせ内容</label><br />
      {errors.content && ( <><span style={{ color: "red" }}>{errors.content.message}</span><br /></> )}
      <textarea id='content'
        style={{ width: "20%" }}
        {...register("content", { required: '入力が必須の項目です' })}
      /><br />

      <input type='checkbox' id='accepted'
        {...register("accepted", { required: true })}
      />
      <label>
        <span style={{ color: errors.accepted ? "red" : "inherited"}}>
          利用規約に同意し{errors.accepted ? "てください" : "ました" }
        </span><br />
      </label><br />
      <input type="submit" value="送信する" />
    </form>
  )

emailフィールドには必須入力のバリデーションに加えて、メールアドレスの入力形式のバリデーションを正規表現を使って実装しました。

バリデーションエラー発生時の動きとしては以下の通りです。

カスタムコンポーネントをReact Hook Form内で利用する

ここまでReact Hook Formを利用し、バリデーション機能を持った簡易的な入力フォームを実装してきました。

上記サンプルコードの通り、フォームの各フィールドの状態を管理しようとする場合、input要素に直接register関数を適用する必要があるため、例えばチェックボックスのような自作で頑張ってスタイルを整えたコンポーネントやChakra UIやmuiなどの外部UIコンポーネントをこのフォームに組み込んで使用するのは諦めるしかないと考えていました。
ところが今回React Hook Formについて改めて調べてみると、Controllerコンポーネントというものを使用すれば外部・自作のUIコンポーネントを統合することができるということを知りました。

以下にControllerコンポーネントを用いて外部のコンポーネントをフォームに統合する例を示します。今回は外部のコンポーネントとしてmuiからCheckboxコンポーネントをインポートしました。
useFormの初期化時にcontrolを取得し、それをControllerコンポーネントに渡しています。そして、Controllerコンポーネントのrenderオプションにインポートしたコンポーネントを返すように設定し、そのコンポーネントにonChangevalueを渡せばOKです。

import { Controller, useForm } from "react-hook-form";
import Checkbox from '@mui/material/Checkbox'

type InputTypes = {
  name: string;
  furigana: string;
  email: string;
  content: string;
  accepted: boolean;
}

const TestForm = () => {
  // useFormの初期化
  const { register, handleSubmit, control, formState: { errors } } = useForm<InputTypes>();

  const onsubmit = async (data: InputTypes) => {
    console.log(data)
     };

  return (
    <form onSubmit={handleSubmit(onsubmit)} style={{ margin: "20px 40px", width: "100%" }}>
      <label htmlFor='name'>お名前</label><br />
      {errors.name && ( <><span style={{ color: "red" }}>{errors.name.message}</span><br /></> )}
      <input type='text' id='name'
        style={{ width: "20%" }}
        {...register("name", { required: '入力が必須の項目です' })}
      /><br />

      <label htmlFor='furigana'>フリガナ</label><br />
      {errors.furigana && ( <><span style={{ color: "red" }}>{errors.furigana.message}</span><br /></> )}
      <input type='text' id='furigana'
        style={{ width: "20%" }}
        {...register("furigana", { required: '入力が必須の項目です' })}
      /><br />

      <label htmlFor='email'>メールアドレス</label><br />
      {errors.email && ( <><span style={{ color: "red" }}>{errors.email.message}</span><br /></> )}
      <input type='email' id='email'
        style={{ width: "20%" }}
        {...register(
          "email",
          {
            required: '入力が必須の項目です',
            pattern: {
              value: /[\w\-._]+@[\w\-._]+\.[A-Za-z]+/,
              message: 'メールアドレスの形式が正しくありません'
            }
          }
        )}
      /><br />

      <label htmlFor='content'>お問い合わせ内容</label><br />
      {errors.content && ( <><span style={{ color: "red" }}>{errors.content.message}</span><br /></> )}
      <textarea id='content'
        style={{ width: "20%" }}
        {...register("content", { required: '入力が必須の項目です' })}
      /><br />

      <Controller
        name="accepted"
        control={control}
        defaultValue={false}
        rules={{ required: true }}
        render={({ field: { onChange, value } }) => <Checkbox onChange={onChange} value={value} />}
      />
      <label>
        <span style={{ color: errors.accepted ? "red" : "inherited"}}>
          利用規約に同意し{errors.accepted ? "てください" : "ました" }
        </span><br />
      </label><br />
      <input type="submit" value="送信する" />
    </form>
  )
}

export default TestForm

以下のようにmuiが提供するCheckboxコンポーネントをフォームに統合することができました。(見た目としては、わかりにくいですね…。)

以上、React Hook Formを使ってバリデーションフォームを作成する方法について見てきました。次回はEmailJSというツールを使って、このフォームのsubmit時のコールバック関数内に、メール送信機能を追加する方法をまとめたいと思います。

ここまで、お読みいただきありがとうございました。ではまた。

Discussion