はじめに
[※注意] 当方React学習歴半年未満のプログラミング初心者です。
現在エンジニア転職を目指しポートフォリオを制作中なのですが、
最近よく耳にするreact-hook-form
をログインやサインインの機能に使ってみました。
これがすぐに理解でき、とても簡単にバリデーションを実装することができたので備忘録を兼ねてご紹介します。
拙い点がありましたら、ご指摘いただけると大変幸いです。
※ 筆者都合によりベース環境にはNext.jsとChakra UIを使用しています。
※ TypeScriptも使用していますが勉強中により、一部anyで逃げている箇所があります...
対象読者
- これからポートフォリオなどのフォーム関連機能の実装に取り掛かろうとしている方
- 簡単にアプリのフォームバリデーションを実装したい方
- バリデーションで正規表現を書きたくない方
何を作るのか
(画像はFigmaで作っていたモックです。)
以下の様なごく一般的なサインアップフォームの実装となります。
ライブラリを用いない場合、恐らくパスワードの一致確認、入力確認、Emailのパターンマッチ確認、名前の文字数確認...
など単純ですが中々に面倒ではありそうです。
※なお、今回バリデーションを通ったあとの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を作成します。
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 }
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 }
(デザインと全然違いますがいったんお許しください。)
まだ何のロジックもない側だけのフォームです。
formタグのnoValidate
ではブラウザが標準で出してくる
バリデーションメッセージを無効にしています。(ちょっと鬱陶しいので)
バリデーション実装
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というプロパティを追加しました。
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リポジトリより確認できました。
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の力を
最大限発揮できていないと思うので、継続的に調べつつやっていきたいと思います。
最後まで見ていただきありがとうございました。