😺
react-hook-formを使用してフォームを作成 ー カスタムバリデーション
はじめに
- react-hook-form を使用して登録フォームのサンプルを作成しました
- UI 部分は MUI を使用しています
ライブラリ
ライブラリ | バージョン |
---|---|
react | ^19.0.0 |
react-hook-form | ^7.54.2 |
@mui/material | ^7.0.1 |
vite | ^6.2.0 |
イメージ
フォーム管理ライブラリ
React Hook Form で行いたい機能
状態管理
- 各フィールドの入力値
- バリデーション実施結果
入力フィールドとの紐づけ
- バリデーションルールと入力フィールドとの紐づけ
- 入力値格納オブジェクトと入力フィールドの紐づけ
- 入力した値は入力値格納オブジェクトに自動で格納
バリデーション
- バリデーションルールに従って自動でバリデーションを実施
- バリデーションルールを任意にカスタマイズ
- バリデーション実施タイミングを任意にカスタマイズ
UI部分
部品化
名称 | コンポーネント | 機能 |
---|---|---|
フォームコンポーネント | UserFromLayout | フォームのレイアウト |
入力コンポーネント | TextFild | テキスト入力 |
入力コンポーネント | PasswordFild | パスワード入力 |
ボタンコンポーネント | SubmitButton | サブミットボタン |
MUI
名称 | MUI | DOM |
---|---|---|
入力フォーム | Box component=form | form |
入力フィールド | TextField type=text | input type=text |
入力フィールド | TextField type=password | input type=password |
サブミットボタン | Button type=submit | button type=submit |
return (
<UserFromLayout
title = {'利用者登録'}
onSubmit = {reactHookForm.handleSubmit(submit)}
>
<ReactHookForm.FormProvider {...reactHookForm}>
<TextFild<LocalUserRegistrationRequestObject>
name = {'loginId'}
label = {'ログインID'}
autoComplete = {'username'}
registerProps = {loginIdRegisterProps}
/>
<PasswordFild<LocalUserRegistrationRequestObject>
name = {'password'}
label = {'パスワード'}
autoComplete = {'none'}
registerProps = {passwordRegisterProps}
/>
<PasswordFild<LocalUserRegistrationRequestObject>
name = {'confirmPassword'}
label = {'パスワード(確認用)'}
autoComplete = {'none'}
registerProps = {confirmPasswordRegisterProps}
/>
<TextFild<LocalUserRegistrationRequestObject>
name = {'nickName'}
label = {'ニックネーム'}
autoComplete = {'nickname'}
registerProps = {nickNameRegisterProps}
/>
<SubmitButton
label = {'登録'}
icon = {ICONS.AppRegistration}
/>
</ReactHookForm.FormProvider>
</UserFromLayout>
)
HOOKS部分
React Hook Form と入力コンポーネントの紐づけ
useForm フックの作成
React Hook Form の useForm フックのオプション
const reactHookForm = ReactHookForm.useForm<LocalUserRegistrationRequestObject>({
mode: 'onSubmit',
reValidateMode: 'onChange',
delayError: 200,
defaultValues: {
loginId: '',
password: '',
confirmPassword: '',
nickName: '',
} as LocalUserRegistrationRequestObject,
})
mode
- 登録ボタン押下前のバリデーションを実施するタイミングを設定
値 | タイミング |
---|---|
onSubmit | 入力フォームの onSubmit イベント |
onChange | 入力フィールドの onChange イベント |
onBlur | 入力フィールドの onBlur イベント |
onTouched | 最初の onBlur イベント、その後は、onChage イベント |
all | onChange イベントと onBlur イベントの両方 |
reValidateMode
- 登録ボタン押下後のバリデーションを実施するタイミングを設定
値 | タイミング |
---|---|
onSubmit | onSubmit = onChange |
onChange | 入力フィールドの onChange イベント |
onBlur | 入力フィールドの onBlur イベント |
delayError
- エラーメッセ時の表示をバリデーションエラーを検知した時点から遅らせる設定(ミリ秒)
defaultValues
- 入力フィールドの初期値を設定
FormProvider に登録
- useForm フックを React Hook Form の FormProvider に登録
<ReactHookForm.FormProvider {...reactHookForm}>
~~~省略~~~
</ReactHookForm.FormProvider>
入力コンポーネントで参照
- 入力コンポーネント内で React Hook Form の useFormContext を使用し FormProvider に登録した useForm フックを参照
export function TextFild<_T extends ReactHookForm.FieldValues = Record<string, unknown>>(props: Props<_T>) {
const reactHookForm = ReactHookForm.useFormContext<_T>()
~~~省略~~~
}
入力値格納オブジェクトのキーやバリデーションルールと入力フィールドとの紐づけ
バリデーションルール
- バリデーションルール( registerProps )を定義
const loginIdRegisterProps = {
validate: (value) => validation(value, {
required: {
message: 'ログインIDを入力してください',
},
maxLength: {
conditions: MatchingPatterns.DB_ASCII_VARCHAR_MAX,
message: `${MatchingPatterns.DB_ASCII_VARCHAR_MAX}文字以内で入力してください`,
},
rules: [
{
isAscii: {
message: '半角英数字記号で入力してください',
},
},
{
matche: {
conditions: MatchingPatterns.USER_LOGIN_ID_ALLOWED_CHARACTERS,
message: '記号は @ - _ . のみ使用できます',
},
},
],
}),
setValueAs: (value) => value.trim(),
} as FormTypes.StringRegisterOptions
validate
- カスタムバリデーションを実施するコールバック関数
- サンプルでは validation コールバック関数を登録
setValueAs
- 入力フィールドのバリデーションを実施する前に実行されるコールバック関数
- サンプルでは入力値の前後の空白スペースを削除
入力コンポーネントに登録
- 入力値格納オブジェクトのキー、および、バリデーションルールを入力コンポーネントに登録
<TextFild<LocalUserRegistrationRequestObject>
name = {'loginId'}
~~~省略~~~
registerProps = {loginIdRegisterProps}
~~~省略~~~
/>
入力コンポーネントのプロパティ
name
- 入力値格納オブジェクトのキーを登録
- 入力した値やバリデーション実施結果を参照する時に使用
registerProps
- バリデーションルールを登録
入力フィールドに登録
- React Hook Form の register 関数を使用して MUI TextField に登録
export function TextFild<_T extends ReactHookForm.FieldValues = Record<string, unknown>>(props: Props<_T>) {
const reactHookForm = ReactHookForm.useFormContext<_T>()
~~~省略~~~
return (
<MUI.TextField
~~~省略~~~
{...reactHookForm.register(props.name, props.registerProps)}
~~~省略~~~
/>
)
}
register 関数のプロパティ
name
- 入力値格納オブジェクトのキーを登録
- 入力値格納オブジェクトと入力フィールドが紐づく
options
- バリデーションルールを登録
- バリデーションルールと入力フィールドが紐づく
React Hook Form とフォームの紐づけ
フォームコンポーネントに登録
- React Hook Form の handleSubmit コールバック関数をフォームコンポーネントに登録
<UserFromLayout
~~~省略~~~
onSubmit = {reactHookForm.handleSubmit(submit)}
>
~~~省略~~~
入力フォームに登録
- フォームコンポーネントに登録した handleSubmit コールバック関数を MUI form の onSubmit に登録
export function UserFromLayout(props: Props) {
~~~省略~~~
return (
~~~省略~~~
<MUI.Box component="form" onSubmit = {props.onSubmit} sx = {{ mt: 4, width: '100%'}} >
~~~省略~~~
</MUI.Box>
</MUI.Container>
~~~省略~~~
)
}
バリデーション(onSubmit)
登録ボタン押下
- 登録ボタンを押下(入力フォームの onSubmit イベントが発生)で、React Hook Form の handleSubmit コールバック関数が実行される
- handleSubmit 実行中は、React Hook Form の formState の isSubmitting に true が設定される
バリデーション実施
- handleSubmit 内で、バリデーションルールに登録した validation コールバック関数が実行される
バリデーション実施結果
- バリデーション実施結果は、React Hook Form の formState に格納される
エラーメッセージ表示
- 入力コンポーネント内で formState を参照しエラーメッセージを表示
export function TextFild<_T extends ReactHookForm.FieldValues = Record<string, unknown>>(props: Props<_T>) {
const reactHookForm = ReactHookForm.useFormContext<_T>()
const helperText = React.useMemo<string|undefined>(() => reactHookForm.formState.errors[props.name]?.message?.toString(), [reactHookForm.formState, props.name])
~~~省略~~~
return (
<MUI.TextField
~~~省略~~~
error = {!!helperText}
helperText = {helperText}
/>
)
}
バリデーションエラーの表示イメージ
バリデーション(onChange)
文字入力
- 文字入力(入力フィールドの onChange イベントが発生)すると、React Hook Form の onChange コールバック関数が実行される
バリデーション実施
- onChange 内で、バリデーションルールに登録した validation コールバック関数が実行される
バリデーション実施結果
- バリデーション実施結果は、React Hook Form の formState に格納される
エラーメッセージ表示
- 入力コンポーネント内で formState を参照しエラーメッセージを表示
個別バリデーション
- パスワードとパスワード(確認用)の様に関連する入力フィールドが存在する場合は、個別のバリデーション処理を記述
パスワード(確認用)のバリデーションルール
- React Hook Form の watch 関数を使用してパスワードフィールドの入力値を参照
const confirmPasswordRegisterProps = {
validate: (value) => validation(value, {
~~~省略~~~
isEquale: {
conditions: reactHookForm.getValues('password'),
message: 'パスワードが異なっています',
},
}),
setValueAs: (value) => value.trim(),
} as FormTypes.StringRegisterOptions
パスワード入力時のバリデーション
- React Hook Form の watch 関数を使用してパスワード入力値の変化をリアルタイムに検出
- 登録ボタンを押下後は React Hook Form の trigger 関数を使用してパスワード(確認用)フィールドのバリデーションを実施
const currentPassword = reactHookForm.watch('password')
const {isSubmitted} = reactHookForm.formState
const {trigger} = reactHookForm
React.useEffect(() => {
if (isSubmitted) {
trigger('confirmPassword')
}
}, [currentPassword, isSubmitted, trigger])
サブミット
登録ボタン押下
- 登録ボタンを押下(入力フォームの onSubmit イベントが発生)で、React Hook Form の handleSubmit コールバック関数が実行される
フォームの非活性化
- handleSubmit 実行中は、React Hook Form の formState の isSubmitting に true が設定される
- 各入力コンポーネント内で isSubmitting を MUI TextField に登録
- ボタンコンポーネント内で isSubmitting を MUI Button に登録
export function SubmitButton<_T extends ReactHookForm.FieldValues = Record<string, unknown>>(props: Props) {
const reactHookForm = ReactHookForm.useFormContext<_T>()
const {isSubmitting} = reactHookForm.formState
return (
<MUI.Box sx = {{ display: 'flex', justifyContent: 'flex-end' }}>
<MUI.Button
~~~省略~~~
disabled = {isSubmitting}
loading = {isSubmitting}
>
~~~省略~~~
</MUI.Button>
</MUI.Box>
)
}
バリデーション実施
- handleSubmit 内で、バリデーションルールに登録した validation コールバック関数が実行される
バリデーション実施結果判定
- バリデーションエラーが無ければ、React Hook Form の handleSubmit に登録した、submit コールバック関数が実行される
登録処理
- 各フィールドで入力した値は、入力値格納オブジェクトに集約して格納されている
- submit 内で registration 関数を実行して、利用者登録処理を実施
const submit: ReactHookForm.SubmitHandler<LocalUserRegistrationRequestObject> = async (localUserRegistrationRequestObject: LocalUserRegistrationRequestObject): Promise<void> => {
return await sessionContext.registration({
loginId: localUserRegistrationRequestObject.loginId,
password: localUserRegistrationRequestObject.password,
nickName: localUserRegistrationRequestObject.nickName,
})
}
- サンプルでは registration 内で例外処理を行う
const registration = async (userRegistrationRequestObject: UserRegistrationRequestObject): Promise<void> => {
~~~省略~~~
try {
const userAuthenticatedObject = await UserApi.registration(userRegistrationRequestObject)
~~~省略~~~
} catch(error: unknown) {
~~~省略~~~
}
}
ソースコード
- 利用者登録コンポーネント(サンプル)のソースコード
import React from 'react'
import * as ReactHookForm from 'react-hook-form'
import * as ICONS from '@mui/icons-material'
import * as SessionContext from '@/features/hooks/contexts/session.context'
import * as FormTypes from '@/components/parts/form/form.types'
import {validation} from '@/features/validator/validator'
import {MatchingPatterns} from '@/features/validator/matching.patterns'
import {UserFromLayout} from '@/components/user/user.form.layout'
import {TextFild} from '@/components/parts/form/text.fild'
import {PasswordFild} from '@/components/parts/form/password.fild'
import {SubmitButton} from '@/components/parts/form/submit.button'
interface LocalUserRegistrationRequestObject extends SessionContext.UserRegistrationRequestObject {
confirmPassword: string,
}
export function UserRegist() {
const sessionContext = SessionContext.useContext()
const reactHookForm = ReactHookForm.useForm<LocalUserRegistrationRequestObject>({
mode: 'onSubmit',
reValidateMode: 'onChange',
delayError: 200,
defaultValues: {
loginId: '',
password: '',
confirmPassword: '',
nickName: '',
} as LocalUserRegistrationRequestObject,
})
const loginIdRegisterProps = {
validate: (value) => validation(value, {
required: {
message: 'ログインIDを入力してください',
},
maxLength: {
conditions: MatchingPatterns.DB_ASCII_VARCHAR_MAX,
message: `${MatchingPatterns.DB_ASCII_VARCHAR_MAX}文字以内で入力してください`,
},
rules: [
{
isAscii: {
message: '半角英数字記号で入力してください',
},
},
{
matche: {
conditions: MatchingPatterns.USER_LOGIN_ID_ALLOWED_CHARACTERS,
message: '記号は @ - _ . のみ使用できます',
},
},
],
}),
setValueAs: (value) => value.trim(),
} as FormTypes.StringRegisterOptions
const passwordRegisterProps ={
validate: (value) => validation(value, {
required: {
message: 'パスワードを入力してください',
},
minLength: {
conditions: MatchingPatterns.USER_PASSWORD_MIN_LENGTH,
message: `${MatchingPatterns.USER_PASSWORD_MIN_LENGTH}文字以上で入力してください`,
},
maxLength: {
conditions: MatchingPatterns.DB_ASCII_VARCHAR_MAX,
message: `${MatchingPatterns.DB_ASCII_VARCHAR_MAX}文字以内で入力してください`,
},
rules: [
{
isAscii: {
message: '半角英数字記号で入力してください',
},
},
{
matche: {
conditions: MatchingPatterns.USER_PASSWORd_INCLUDE_LOWERCASE,
message: '小文字(a ~ z)を含めてください',
},
},
{
matche: {
conditions: MatchingPatterns.USER_PASSWORd_INCLUDE_UPPERCASEE,
message: '大文字(A ~ Z)を含めてください',
},
},
{
matche: {
conditions: MatchingPatterns.USER_PASSWORd_INCLUDE_NUMBER,
message: '数字(0 ~ 9)を含めてください',
},
},
{
matche: {
conditions: MatchingPatterns.USER_PASSWORd_INCLUDE_SYMBOL,
message: '記号を含めてください',
},
},
],
}),
setValueAs: (value) => value.trim(),
} as FormTypes.StringRegisterOptions
const confirmPasswordRegisterProps = {
validate: (value) => validation(value, {
required: {
message: 'パスワード(確認用)を入力してください',
},
isEquale: {
conditions: reactHookForm.getValues('password'),
message: 'パスワードが異なっています',
},
}),
setValueAs: (value) => value.trim(),
} as FormTypes.StringRegisterOptions
const nickNameRegisterProps = {
validate: (value) => validation(value, {
required: {
message: 'ニックネームを入力してください',
},
maxLength: {
conditions: MatchingPatterns.DB_WIDTH_VARCHAR_MAX,
message: `${MatchingPatterns.DB_WIDTH_VARCHAR_MAX}文字以内で入力してください`,
},
}),
setValueAs: (value) => value.trim(),
} as FormTypes.StringRegisterOptions
const currentPassword = reactHookForm.watch('password')
const {isSubmitted} = reactHookForm.formState
const {trigger} = reactHookForm
React.useEffect(() => {
if (isSubmitted) {
trigger('confirmPassword')
}
}, [currentPassword, isSubmitted, trigger])
const submit: ReactHookForm.SubmitHandler<LocalUserRegistrationRequestObject> = async (localUserRegistrationRequestObject: LocalUserRegistrationRequestObject): Promise<void> => {
return await sessionContext.registration({
loginId: localUserRegistrationRequestObject.loginId,
password: localUserRegistrationRequestObject.password,
nickName: localUserRegistrationRequestObject.nickName,
})
}
return (
<UserFromLayout
title = {'利用者登録'}
onSubmit = {reactHookForm.handleSubmit(submit)}
>
<ReactHookForm.FormProvider {...reactHookForm}>
<TextFild<LocalUserRegistrationRequestObject>
name = {'loginId'}
label = {'ログインID'}
autoComplete = {'username'}
registerProps = {loginIdRegisterProps}
/>
<PasswordFild<LocalUserRegistrationRequestObject>
name = {'password'}
label = {'パスワード'}
autoComplete = {'none'}
registerProps = {passwordRegisterProps}
/>
<PasswordFild<LocalUserRegistrationRequestObject>
name = {'confirmPassword'}
label = {'パスワード(確認用)'}
autoComplete = {'none'}
registerProps = {confirmPasswordRegisterProps}
/>
<TextFild<LocalUserRegistrationRequestObject>
name = {'nickName'}
label = {'ニックネーム'}
autoComplete = {'nickname'}
registerProps = {nickNameRegisterProps}
/>
<SubmitButton
label = {'登録'}
icon = {ICONS.AppRegistration}
/>
</ReactHookForm.FormProvider>
</UserFromLayout>
)
}
Discussion