LoginSignup
13
9

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-04-10

はじめに

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

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

対象読者

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

何を作るのか

(画像はFigmaで作っていたモックです。)
以下の様なごく一般的なサインアップフォームの実装となります。
ライブラリを用いない場合、恐らくパスワードの一致確認、入力確認、Emailのパターンマッチ確認、名前の文字数確認...
など単純ですが中々に面倒ではありそうです。
image.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などが古い書き方のままになっています。
こちらの大変ありがたい記事は事前に見ておくといいでしょう。

実装

スタイルのみの実装

※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

(デザインと全然違いますがいったんお許しください。)
まだ何のロジックもない側だけのフォームです。
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は必ず必要なもので、yupyupResolverについては
バリデーションを楽にしてくれるものと考えています。

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

useFormの引数をこのように指定し、3つの項目を受け取ります。

register: 入力された値をhooksに伝播させます。
handleSubmit: form内でsubmitが起きた際の処理を記述します。
errors: 当該のformでバリデーションエラーが起きた際のエラーメッセージ等を格納します

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

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

さいごに

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

13
9
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
13
9