5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React + Typescript 〜react-hook-formを使って超簡単にフォームvalidationを実装する〜

Last updated at Posted at 2023-01-29

はじめに

筆者は現在、FJORD BOOT CAMP(フィヨルドブートキャンプ)にてテイスティングノートという自作サービスの開発をしています。

フロントエンド開発において回答フォームを作る必要があり、その際にreact-hook-formという便利なパッケージを使用したので、その使い方についてまとめようと思います。

対象読者

  • react開発でフォームの実装を検討中の方
  • react-hook-formの概要について知りたい方

react-hook-formとは

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

一言でまとめると、超簡単にvalidationが実装できるnpmです。
その名の通り、reactプロジェクトで使用できるnpmでフォームの実装にとても便利なhooksを提供してくれます。

この記事のゴール

ハンズオン形式でreact-hook-formの使い方を解説します。
読者の方に「めっちゃ便利じゃん」と思っていただけることがゴールです。

今回はcreate-react-appを使用してReact + Typescriptで開発を行います。

完成品

以下のようなユーザー情報を入力するようなフォームを実装していきます。

7e680ad0da2b9ec11e5ae982ceeff5a6_AdobeExpress.gif
画像がちいさくてすみません😥

各inputにvalidationが設定されていて、入力値をタイムリーに判定してエラーメッセージを表示してくれるような仕様です。

尚、react-hook-formの使い方に関する記事のため、フォーム送信ボタン押下以降(APIリクエストなど)については触れませんのでご了承ください。

実装 〜react-hook-form〜

react-hook-formの使い方からまとめていきます。
まずはcreate-react-appでプロジェクトを立ち上げましょう。
今回はTypescriptで進めいていきます。

プロジェクト作成後にreact-hook-formのインストールも済ませてしまいます。

% npx create-react-app --template typescript react-form-app
% cd react-form-app
% npm i react-hook-form
{
  "name": "react-form-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.5.2",
    "@types/node": "^16.18.11",
    "@types/react": "^18.0.27",
    "@types/react-dom": "^18.0.10",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    // インストール
    "react-hook-form": "^7.42.1",
    "react-scripts": "5.0.1",
    "typescript": "^4.9.4",
    "web-vitals": "^2.1.4"
  },
  // 以下省略

react-hook-formを使うための準備はこれだけです。

フォームのデータ構造を設計する

フォームで扱うデータ構造を定義します。
会員登録ページを想定して以下のように定義しました。

カラム名 データ型 制約
name string NOT NULL
nickname string NOT NULL
password string NOT NULL
password_confirmation string NOT NULL
src/types/user.ts
type User = {
 name: string
 nickname: string
 password: string
 passwordConfirmation: string
}

useForm

それでは実際にフォームを実装していきます。
まずはreact-hook-formが提供しているuseFormを使って必要なメソッドやオブジェクトを取得します。

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

import { User } from "../types/user"

const Form = () => {
 const {
  handleSubmit,
  register,
  formState: {
   errors,
   isValid,
   isSubmitting
  }
 } = useForm<User>()
}

useFormから取得できるメソッドについてはこちらで確認ができます。
今回は下記を取得しています。

メソッド名 概要
register react-hook-formで管理したい要素の登録、validationの設定が行える
handleSubmit formタグのonSubmitイベントに登録する関数
formState フォームで管理しているステート情報を含むオブジェクト
errors validation errorの場合のerror情報を含んでいるオブジェクト
isValid validationの結果を真偽値で返す
isSubmitting hansleSubmit実行中にtrueを返す

それぞれの使い方は後ほど解説していきます。

Typescriptを使用する場合、useFormを実行する際に型引数を与える必要があります。
この型引数にはreact-hook-formで管理したいデータの型を与えましょう。

今回の例では先ほど用意したUserを与えています。

handleSubmit

フォームのonSubmitイベントに渡す関数です。
引数にSubmitHandler型の関数を渡すことでフォームが送信された時の処理を実装できます。

SubmitHandler

handleSubmitの引数に渡す関数の型です。
型引数にuseForm呼び出しの際に与えた型と同じ型を与えます。

引数にreact-hook-formで管理しているdataオブジェクトが渡ってくるので、このdataをAPIでPOSTしたりといった処理を自前で用意できます。
今回はSubmitされたら入力されたデータをconsoleへ表示させます。

import { SubmitHandler, useForm } from "react-hook-form"
import { User } from "../types/user"

const Form = () => {
 const {
  handleSubmit,
  register,
  formState: {
   errors,
   isValid,
   isSubmitting
  }
 } = useForm<User>()
 const onSubmit: SubmitHandler<User> = (data) => console.log(data)
}

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

register

https://react-hook-form.com/api/useform/register

input、selectなどのHTML要素をreact-hook-formの管理下に登録するためのメソッドです。

<label htmlFor="name">
 ユーザー名
 <input type="text" id="name" { ...register('name', { required: true }) } />
</label>

registerはname、ref、onChange、onBlurをプロパティに持つオブジェクトを返却するメソッドなので、{ ...register }とすることで返却されるオブジェクトが展開されて、HTML要素の属性として指定されます。

registerの引数にはname、ref、onChange、onBlur、optionsを指定することができます。
詳しい使い方については公式ドキュメントをご確認いただければと思います。

https://react-hook-form.com/api/useform/register

基本的な使い方としては第一引数にnameを与えて、第二引数に入力必須やvalidationメソッドを指定したオプション(オブジェクト)を与えるといった感じです。

第一引数にはuseFormを実行した際に与えた型引数に応じたnameを指定できます。
今回の場合はname、nickname、password、passwordConfirmationです。

それではこの4つのフィールドをregisterで登録していきます。

<form onSubmit={handleSubmit(onSubmit)}>
  <label htmlFor="name">
    ユーザー名
    <input
      type="text"
      id="name"
      { ...register('name', { required: true }) }
    />
  </label>
  
  <label htmlFor="nickname">
    ニックネーム
    <input
      type="text"
      id="nickname"
      { ...register('nickname', { required: true }) }
    />
  </label>
  
  <label htmlFor="password">
    パスワード
    <input
      type="password"
      id="password"
      { ...register('password', { required: true }) }
    />
  </label>
  
  <label htmlFor="passwordConfirmation">
    確認用
    <input
      type="password"
      id="passwordConfirmation"
      { ...register('passwordConfirmation', { required: true }) }
    />
</form>

現時点ではどのフィールドも入力必須のみを指定していますが、後ほど個別にvalidationルールを登録していきます。

errors

react-hook-formで管理している要素のvalidation errorの結果を保持するオブジェクトです。
errors.nameのように各プロパティにアクセスができます。

今回はvalidation errorがあったら各inputタグの下にエラーメッセージを表示するという実装を行います。

<form onSubmit={handleSubmit(onSubmit)}>
    <label htmlFor="name">
      ユーザー名
      <input
        type="text"
        id="name"
        { ...register('name', { required: true }) }
      />
    </label>
    {errors.name &&
      <span>入力必須です</span>
    }

  <label htmlFor="nickname">
    ニックネーム
    <input
      type="text"
      id="nickname"
      { ...register('nickname', { required: true }) }
    />
  </label>
 {errors.nickname &&
   <span>入力必須です</span>
 }

  <label htmlFor="password">
    パスワード
    <input
      type="password"
      id="password"
      { ...register('password', { required: true }) }
    />
  </label>
  {errors.password &&
    <span>入力必須です</span>
  }

  <label htmlFor="passwordConfirmation">
    確認用
    <input
      type="password"
      id="passwordConfirmation"
      { ...register('passwordConfirmation', { required: true }) }
    />
  </label>
  {errors.passwordConfirmation &&
    <span>入力必須です</span>
  }
</form>

ここまでで一旦挙動を確認してみましょう。
formの閉じタグの前にサブミット用の下記inputタグを追加して実際にボタンを押して確認します。

<input type="submit" value="提出する" />
  • validationエラーなし
    handleSubmitに与えたonSubmitが走ってconsoleにdataが表示されています。
    b1e496cc7150e83cbd826e1c4be894f2_AdobeExpress.gif

  • validationエラーあり
    エラーメッセージが正しく表示されています。
    30ff00b6f6daf03b7f717780e2d6789c_AdobeExpress.gif
    ※若干スタイルを当てています

!isValid || isSubmitting

isValid、isSubmittingについては先に記述した概要通りですが、下記のようにすることでバリデーションエラーがある場合にSubmitボタンを押せないようにすることが可能です。

<input type="submit" value="提出する" disabled={!isValid || isSubmitting} />

a2dffc0cffc7a8cf1549b0d72c73449e_AdobeExpress.gif

必須項目をすべて入力するまでボタンが非活性になっているのがわかります。
ただこのままだとボタンがなぜ非活性になっているのかユーザーはわからないので、エラーメッセージをタイムリーに表示する方がベターです。

その場合はuseForm実行時の引数を与えることで実現ができます。

const {
  handleSubmit,
  register,
  formState: {
   errors,
   isValid,
   isSubmitting
  }
 } = useForm<User>({ mode: 'onChange' })

modeはvalidationを走らせるタイミングを指定できるオプションです。
他にもonBlurやonTouchedがあります。デフォルトはonSubmitです。

これにonChangeを渡すことで入力内容の変更を検知してvalidationが走ってくれます。

b53ff166a43db7a4f7aa9b16a2b29f45_AdobeExpress.gif

このようにSubmitボタンにdisabledを指定する場合はmodeにonChangeを指定することをお勧めします。

useFormの引数については下記を参照してください。

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

validationルールの設定

それでは最後に各inputに個別のvalidationルールを設定します。
registerの第二引数にルールの設定を記述したオブジェクトを渡すことで実現ができます。
今回は下記のようにルールを設定します。

name validationルール
name 入力必須
nickname 4文字以上
password 6文字以上の英数字
passwordConfirmation passwordと同じ値

順番に実装していきましょう。

required

trueを値に指定することで入力必須項目に設定できます。

<label htmlFor="name">
  ユーザー名
  <input
    type="text"
    id="name"
    { ...register('name', { required: true }) }
  />
</label>
{errors.name &&
  <span>入力必須です</span>
}

minLength

数値を値に指定することで入力文字数の最小値を設定できます。

<label htmlFor="nickname">
  ニックネーム
  <input
   type="text"
   id="nickname"
   { ...register('nickname', { minLength: 4 }) }
  />
</label>
{errors.nickname &&
 <span>4文字以上で入力してください</span>
}

d1b4f6e88cc9e3d3515e36da504f8f81_AdobeExpress.gif

pattern

正規表現を値に指定しすることでパターンマッチしない場合にvalidation errorを発生させてくれます。

<label htmlFor="password">
  パスワード
  <input
    type="text"
    id="password"
    { ...register('password', { pattern: /\w{6,}/ }) }
  />
</label>
{errors.password &&
  <span>半角英数字6文字以上で入力してください</span>
}

正規表現についての説明は割愛させていただきます。
(上記例の場合だと英字のみ数字のみも通ってしまいますがこのまま進めます)
5bca58f4d43aa943485f11f298e0a8f4_AdobeExpress.gif
※分かりやすくするためにinputタグをtype="text"としています。

validate

バリデーションルールを自前で設定することができるプロパティです。
validateプロパティにの値には関数を指定することができ、指定した関数の返却値としてvalidationのルールを設定します。
この関数の引数には入力値valueが渡ってきます。

今回のケースではパスワードの入力値と確認用の入力値を比べて===かどうかを判定する設定を書いていきます。
ここで、パスワードの入力値を取得する必要があるため、useFormが提供しているgetValuesメソッドを使用します。

getValues

https://react-hook-form.com/api/useform/getvalues

引数にuseFormで登録したnameを与えることで、その入力値をタイムリーに取得できます。
今回のケースではgetValues('password')のように呼び出します。

最終的な実装は下記です。

const {
  handleSubmit,
  register,
  // 追加
  getValues,
  formState: {
    errors,
    isValid,
    isSubmitting
  }
} = useForm<User>({ mode: 'onChange' })

return (
// 省略
  <label htmlFor="passwordConfirmation">
    確認用
    <input
      type="text"
      id="passwordConfirmation"
      { ...register('passwordConfirmation', {
          validate: (value) => value === getValues('password')
        })
      }
    />
  </label>
  {errors.passwordConfirmation &&
    <span>パスワードと一致しません</span>
  }
)

引数に渡ってくる確認用の入力値とgetValuesで取得したパスワードの入力値を比較しています。
これがfalseとなった場合にvalidation errorが発生します。

88e27d93e2e93b0f5510a2b54f09c85b_AdobeExpress.gif

registerに登録できるオプションは他にあるので気になる方は公式ドキュメントをチェックしてみてください。

https://react-hook-form.com/api/useform/register

まとめ

最終的なコードを記載します。

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

import { User } from "../types/user"

const Form = () => {
  const {
    handleSubmit,
    register,
    getValues,
    formState: {
      errors,
      isValid,
      isSubmitting
    }} = useForm<User>({ mode: 'onChange' })

  const onSubmit: SubmitHandler<User> = (data) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label htmlFor="name">
        ユーザー名
        <input
          type="text"
          id="name"
          { ...register('name', { required: true }) }
        />
      </label>
      {errors.name &&
        <span>入力必須です</span>
      }

      <label htmlFor="nickname">
        ニックネーム
        <input
          type="text"
          id="nickname"
          { ...register('nickname', { minLength: 4 }) }
        />
      </label>
      {errors.nickname &&
        <span>4文字以上で入力してください</span>
      }

      <label htmlFor="password">
        パスワード
        <input
          type="text"
          id="password"
          { ...register('password', { pattern: /\w{6,}/ }) }
        />
      </label>
      {errors.password &&
        <span>半角英数字6文字以上で入力してください</span>
      }

      <label htmlFor="passwordConfirmation">
        確認用
        <input
          type="text"
          id="passwordConfirmation"
          { ...register('passwordConfirmation', {
              validate: (value) => (
                value === getValues('password') ||
                'パスワードと一致しません'
              )
            })
          }
        />
      </label>
      {errors.passwordConfirmation &&
        <span>{errors.passwordConfirmation.message}</span>
      }
      <input type="submit" value="提出する" disabled={!isValid || isSubmitting} />
    </form>
  )
}

export default Form

とても縦に長いコードになってしまっていました。
inputタグやエラーメッセージはコンポーネントに切り出すべきですが今回の記事では割愛します。

validationを自前で実装しようとすると、もっと冗長なコードになってしまうかと思いますが、react-hook-formを利用することでとても簡単にvalidationの実装ができます。

react-hook-formは公式ドキュメントも充実しており、比較的英語が苦手な方でも読みやすいのではないかと思います。

React + Formときたらreact-hook-formを一度検討されてはいかがでしょうか。

5
3
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
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?