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

# はじめに

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

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

# 対象読者
- 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930395/346bf2cc-bc25-65b5-70a0-dc8a0ec6a6e4.gif)
画像がちいさくてすみません😥


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

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

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

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

```bash
% npx create-react-app --template typescript react-form-app
% cd react-form-app
% npm i react-hook-form
```
```javascript
{
  "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 |

```typescript:src/types/user.ts
type User = {
 name: string
 nickname: string
 password: string
 passwordConfirmation: string
}
```

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

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

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

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

useFormから取得できるメソッドについては[こちら](https://react-hook-form.com/api/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へ表示させます。

```react
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の管理下に登録するためのメソッドです。

```react
<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で登録していきます。

```react
<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タグの下にエラーメッセージを表示するという実装を行います。

```react
<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タグを追加して実際にボタンを押して確認します。

```react
<input type="submit" value="提出する" />
```

- validationエラーなし
handleSubmitに与えたonSubmitが走ってconsoleにdataが表示されています。
![b1e496cc7150e83cbd826e1c4be894f2_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930395/912ee373-1dc7-7c2f-794e-cc16489734eb.gif)



- validationエラーあり
エラーメッセージが正しく表示されています。
![30ff00b6f6daf03b7f717780e2d6789c_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930395/79d9f611-30bd-8209-7dcd-da18d8c05d72.gif)
※若干スタイルを当てています



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

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

![a2dffc0cffc7a8cf1549b0d72c73449e_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930395/b81f2c8a-5511-aa46-1f72-5486094f6838.gif)

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

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

```react
const {
  handleSubmit,
  register,
  formState: {
   errors,
   isValid,
   isSubmitting
  }
 } = useForm<User>({ mode: 'onChange' })
```
modeはvalidationを走らせるタイミングを指定できるオプションです。
他にもonBlurやonTouchedがあります。デフォルトはonSubmitです。

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

![b53ff166a43db7a4f7aa9b16a2b29f45_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930395/a8bf68e1-d798-78cd-5d0e-fd0dc43d5ece.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を値に指定することで入力必須項目に設定できます。

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

#### minLength
数値を値に指定することで入力文字数の最小値を設定できます。
```react
<label htmlFor="nickname">
  ニックネーム
  <input
   type="text"
   id="nickname"
   { ...register('nickname', { minLength: 4 }) }
  />
</label>
{errors.nickname &&
 <span>4文字以上で入力してください</span>
}
```
![d1b4f6e88cc9e3d3515e36da504f8f81_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930395/5e910059-4f92-c502-2d24-e012d4197a58.gif)


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

```react
<label htmlFor="password">
  パスワード
  <input
    type="text"
    id="password"
    { ...register('password', { pattern: /\w{6,}/ }) }
  />
</label>
{errors.password &&
  <span>半角英数字6文字以上で入力してください</span>
}
```
正規表現についての説明は割愛させていただきます。
（上記例の場合だと英字のみ数字のみも通ってしまいますがこのまま進めます)
![5bca58f4d43aa943485f11f298e0a8f4_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930395/020e6e75-424c-82ae-c90d-420754b9645a.gif)
※分かりやすくするためにinputタグを`type="text"`としています。

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

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

### getValues

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

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

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

```react
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930395/f2b936d0-1faa-5180-fab3-de9909941aa5.gif)

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

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

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

```react
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を一度検討されてはいかがでしょうか。