react-hook-formとyupで超お手軽フォームバリデーション実装

# はじめに
[※注意] 当方React学習歴半年未満のプログラミング初心者です。
現在エンジニア転職を目指しポートフォリオを制作中なのですが、
最近よく耳にする`react-hook-form`をログインやサインインの機能に使ってみました。
これがすぐに理解でき、とても簡単にバリデーションを実装することができたので備忘録を兼ねてご紹介します。
拙い点がありましたら、ご指摘いただけると大変幸いです。

※ 筆者都合によりベース環境にはNext.jsとChakra UIを使用しています。
※ TypeScriptも使用していますが勉強中により、一部anyで逃げている箇所があります...

# 対象読者
- これからポートフォリオなどのフォーム関連機能の実装に取り掛かろうとしている方
- 簡単にアプリのフォームバリデーションを実装したい方
- バリデーションで正規表現を書きたくない方

# 何を作るのか
（画像はFigmaで作っていたモックです。）
以下の様なごく一般的なサインアップフォームの実装となります。
ライブラリを用いない場合、恐らくパスワードの一致確認、入力確認、Emailのパターンマッチ確認、名前の文字数確認...
など単純ですが中々に面倒ではありそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682983/e480b9dd-900b-a26e-3c95-0c798fe889ec.png)

※なお、今回バリデーションを通ったあとのAPI連携処理等は書いていません。

# 準備
```
yarn add react-hook-form @hookform/resolvers yup
```
`react-hook-form`
言わずもがなのフォームバリデーションライブラリです。
コード量を減らし、再レンダリングも抑制してくれつつ、高性能なバリデーションを可能とします。
`@hookform/resolvers`
**yupResolver**をimportして、yupで作成したバリデーションとreact-hook-formを接続します。
`yup` 
各フォーム項目ごとに異なるバリデーションを定義していきます。
宣言的で大変わかりやすいバリデーションが可能です。

本当はreact-hook-formのみでも可能なのですが、
コードがスッキリするかつemailなどの正規表現を`yup`が予め用意してくれている事もあり
今回は使用することにしました。

react-hook-formは結構最近にversionが7にアップデートされたそうです。
書き方が若干変わっているので一部公式のCodesandBoxなどが古い書き方のままになっています。
[こちらの大変ありがたい記事](https://qiita.com/bluebill1049/items/f838bae7f3ed29e81fff)は事前に見ておくといいでしょう。

# 実装
## スタイルのみの実装
※chakra UIを使用している点とコーディングが未熟な点については何卒ご了承ください。
単一のデータ入力の子コンポーネントTextFormと、それを纏めてsubmitボタンを設置するような
親コンポーネントSignupFormを作成します。

```TextForm.tsx
import { Badge, FormControl, FormLabel, Input, Text } from '@chakra-ui/react'
import type { VFC } from 'react'

export type TextFormProps = {
  label: string
  placeholder: string
  name: string
  type: string
  isRequired: boolean
}

const TextForm: VFC<TextFormProps> = (props: TextFormProps) => {
  return (
    <FormControl id={props.name} w="400px">
      <FormLabel m={1}>
        <Text display="inline" fontSize="13px" fontWeight="bold">
          {props.label}
        </Text>{' '}
        {props.isRequired && (
          <Badge bg="red.400" color="white" py="3px" px="5px" borderRadius="7px">
            必須
          </Badge>
        )}
      </FormLabel>

      <Input
        type={props.type}
        placeholder={props.placeholder}
        borderColor="gray.500"
        borderRadius="10px"
        color="gray.700"
        _placeholder={{
          fontSize: '14px',
        }}
      />
    </FormControl>
  )
}

export { TextForm }
```

```SignupForm.tsx
import { VStack } from '@chakra-ui/react'
import type { VFC } from 'react'

import { TextForm } from '@/components/forms/unit'

const SignupForm: VFC = () => {

  return (
    <form name="SignupForm" noValidate>
      <VStack py="30px" bg="gray.100" w="650px" spacing="8" borderRadius="20px">
        <TextForm
          label={'メールアドレス'}
          placeholder={'メールアドレスを入力'}
          name="email"
          type="email"
          isRequired={true}
        />
        <TextForm
          label={'ユーザ名'}
          placeholder={'ユーザ名を入力'}
          name="username"
          type="text"
          isRequired={true}
        />
        <TextForm
          label={'パスワード'}
          placeholder={'パスワードを入力'}
          type="password"
          name="password"
          isRequired={true}
        />
        <TextForm
          label={'パスワード（確認用）'}
          placeholder={'上記と同じパスワードを入力'}
          type="password"
          name="password_confirm"
          isRequired={true}
        />

        <NormalButton
          type="submit"
          width="200px"
          text="登録"
          bg="green.300"
          color="white"
          hover={{ bg: 'green.400' }}
        />
      </VStack>
    </form>
  )
}

export { SignupForm }
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682983/0bae643f-5616-1639-69d9-408eb06077f1.png)

(デザインと全然違いますがいったんお許しください。)
まだ何のロジックもない側だけのフォームです。
formタグの`noValidate`ではブラウザが標準で出してくる
バリデーションメッセージを無効にしています。（ちょっと鬱陶しいので）

## バリデーション実装
```TextForm.tsx
export type TextFormProps = {
  label: string
  placeholder: string
  name: string
  type: string
  isRequired: boolean
+  errorMessage?: string
+  registers?: any
}

const TextForm: VFC<TextFormProps> = (props: TextFormProps) => {
  return (
    <FormControl id={props.name} w="400px">
      <FormLabel m={1}>
        <Text display="inline" fontSize="13px" fontWeight="bold">
          {props.label}
        </Text>{' '}
        {props.isRequired && (
          <Badge bg="red.400" color="white" py="3px" px="5px" borderRadius="7px">
            必須
          </Badge>
        )}
      </FormLabel>

      <Input
        type={props.type}
        placeholder={props.placeholder}
        borderColor="gray.500"
        borderRadius="10px"
        color="gray.700"
        _placeholder={{
          fontSize: '14px',
        }}
+        {...props.registers}
      />
+      <Text color="red.500" fontSize="14px">
+        {props.errorMessage}
+      </Text>
    </FormControl>
  )
}
```
当該のフォームがバリデーションに引っかかった際に格納されるerrorMessageプロパティと、
それがあった際の出力処理
及び親コンポーネント側でのreact-hook-formの生成処理で
各formに設定しなければならないregisterというプロパティを追加しました。

```SignupForm.tsx
import { VStack } from '@chakra-ui/react'
+ import { yupResolver } from '@hookform/resolvers/yup'
import type { VFC } from 'react'
+ import { useForm } from 'react-hook-form'
+ import * as yup from 'yup'

import { NormalButton } from '@/components/common/unit'
import { TextForm } from '@/components/forms/unit'

+ const REQUIRE_MSG = '必須入力項目です'
+ const VIOLATION_EMAIL = '正しい形式で入力してください'
+ const VIOLATION_NAME_COUNT = '名前は16文字以下で入力してください'
+ const VIOLATION_PASSWORD_COUNT = 'パスワードは16文字以下で入力してください'
+ const VIOLATION_PASSWORD_CONFIRM = '入力したパスワードが一致しません'

const SignupSchema = yup.object().shape({
  email: yup.string().required(REQUIRE_MSG).email(VIOLATION_EMAIL),
  username: yup.string().required(REQUIRE_MSG).max(16, VIOLATION_NAME_COUNT),
  password: yup.string().required(REQUIRE_MSG).max(16, VIOLATION_PASSWORD_COUNT),
  password_confirm: yup
    .string()
    .required(REQUIRE_MSG)
    .oneOf([yup.ref('password'), null], VIOLATION_PASSWORD_CONFIRM),
})

+ const SignupForm: VFC = () => {
+   const {
+     register,
+     handleSubmit,
+     formState: { errors },
+   } = useForm({
+     resolver: yupResolver(SignupSchema),
+   })

+   const onSubmit = (data: any) => {
+     // eslint-disable-next-line no-console
+     console.log(data)
+   }

  return (
    <form name="SignupForm" onSubmit={handleSubmit(onSubmit)} noValidate>
      <VStack py="30px" bg="gray.100" w="650px" spacing="8" borderRadius="20px">
        <TextForm
          label={'メールアドレス'}
          placeholder={'メールアドレスを入力'}
          name="email"
          type="email"
          isRequired={true}
+           registers={register('email')}
+           errorMessage={errors.email?.message}
        />
        <TextForm
          label={'ユーザ名'}
          placeholder={'ユーザ名を入力'}
          name="username"
          type="text"
          isRequired={true}
+           registers={register('username')}
+           errorMessage={errors.username?.message}
        />
        <TextForm
          label={'パスワード'}
          placeholder={'パスワードを入力'}
          type="password"
          name="password"
          isRequired={true}
+           registers={register('password')}
+           errorMessage={errors.password?.message}
        />
        <TextForm
          label={'パスワード（確認用）'}
          placeholder={'上記と同じパスワードを入力'}
          type="password"
          name="password_confirm"
          isRequired={true}
+           registers={register('password_confirm')}
+           errorMessage={errors.password_confirm?.message}
        />

        <NormalButton
          type="submit"
          width="200px"
          text="登録"
          bg="green.300"
          color="white"
          hover={{ bg: 'green.400' }}
        />
      </VStack>
    </form>
  )
}

export { SignupForm }
```
追加した`useForm`は必ず必要なもので、`yup`と`yupResolver`については
バリデーションを楽にしてくれるものと考えています。

`SignupSchema`にこのフォームでの各項目に対するバリデーションを指定していきます。
ほぼ説明する必要もないほどわかりやすいんじゃないでしょうか。
抜粋すると、email項目は入力されているかつ、アドレスの正規表現に沿っているか、
password項目は入力されているかつ、16文字以下であるか、を判断しています。
引っかかった箇所の引数に指定しているエラーメッセージが画面に表示されます。
こちらのメソッドはyupの[GitHubリポジトリ](https://github.com/jquense/yup)より確認できました。

`useForm`の引数をこのように指定し、3つの項目を受け取ります。
>register：　入力された値をhooksに伝播させます。
>handleSubmit：　form内でsubmitが起きた際の処理を記述します。
>errors：　当該のformでバリデーションエラーが起きた際のエラーメッセージ等を格納します

そして子コンポーネントにregister関数の実行とerrorMessageを追加で渡します。
筆者はFormコンポーネントを切り出している影響でregisterを[公式サイト](https://react-hook-form.com/jp/get-started/#SchemaValidation)のようには書けないのでこの形で子に渡しました。
`register('★★')`の★★部分はname属性になります。ここは頭の方で定義した`SignupSchema`
のプロパティ名と一致させなければいけない点に注意です。

最後にこのコードの完成動画を載せておきます。
https://twitter.com/shin_k_2281/status/1380922999818252288

# さいごに
フォーム系ライブラリを扱うのはreact-hook-formが初めてでしたが
非常に強力だったので今後も使用していこうと思います。
恐らく今回の使用例ではバージョンアップされたreact-hook-formの力を
最大限発揮できていないと思うので、継続的に調べつつやっていきたいと思います。
最後まで見ていただきありがとうございました。