📝
react-hook-form を使用してフォームを作成 ー カスタムリゾルバ
はじめに
- カスタムリゾルバを作成しました
- 「react-hook-formを使用してフォームを作成 ー カスタムバリデーション」で作成したサンプルフォームを更新しました(カスタムバリデーションからカスタムリゾルバを使用したバリデーションに変更しました)
ライブラリ
ライブラリ | バージョン |
---|---|
react | ^19.0.0 |
react-hook-form | ^7.54.2 |
@hookform/resolvers | ^5.0.1 |
@mui/material | ^7.0.1 |
vite | ^6.2.0 |
イメージ
HOOKS部分
React Hook Form Resolvers
カスタムリゾルバ
import * as ReactHookForm from 'react-hook-form'
import * as HookformResolvers from '@hookform/resolvers'
import {ValidationRules} from '@/features/validator/rules/validation.rules'
import {ValidationOptions} from '@/features/validator/props/varidation.options'
import {ValidationCommands} from '@/features/validator/props/validation.commands'
import {validation} from '@/features/validator/validator'
import getValue from '@/features/validator/react.hook.form/utils/get.value'
import getValidationRules from '@/features/validator/react.hook.form/utils/get.validation.rules'
import buildNames from '@/features/validator/react.hook.form/utils/build.names'
const parseErrorSchema = (errores: Record<string, string[]>, validateAllFieldCriteria: boolean): ReactHookForm.FieldErrors<Record<string, unknown>> => {
return Object.keys(errores).reduce<Record<string, ReactHookForm.FieldError>>((previous, path) => {
if (!previous[path]) {
previous[path] = {
message: errores[path].join('\n'),
type: '',
}
}
if (validateAllFieldCriteria) {
for (const message of errores[path]) {
const types = previous[path].types || {}
previous[path].types = {
...types,
[Object.keys(types).length]: message,
}
}
}
return previous
}, {})
}
export const customResolver = <_S extends Record<keyof _S, ValidationRules> = Record<string, ValidationRules>, _T extends ReactHookForm.FieldValues = Record<string, unknown>> (
schema: _S
): ReactHookForm.Resolver<_T> => {
return async (objet, _, options) => {
const validateAllFieldCriteria = !options.shouldUseNativeValidation && options.criteriaMode === 'all'
const names = validateAllFieldCriteria
? buildNames(objet)
: options.names || []
const validationOptions: ValidationOptions = {
abortFirstError: !validateAllFieldCriteria,
}
const validationCommands: ValidationCommands = {
ref: (name) => {
return getValue(objet, name)
},
}
const errores: Record<string, string[]> = {}
for (const name of names) {
const validationRules = getValidationRules(schema, name)
if (validationRules) {
const result = await validation({
value: getValue(objet, name),
rules: validationRules,
options: validationOptions,
commands: validationCommands,
})
if (result.length) {
errores[name] = result
}
}
}
if (Object.keys(errores).length) {
return {
values: {},
errors: HookformResolvers.toNestErrors(
parseErrorSchema(errores, validateAllFieldCriteria),
options
),
}
}
if (options.shouldUseNativeValidation) {
HookformResolvers.validateFieldsNatively({}, options)
}
return {
values: Object.assign({}, objet),
errors: {},
}
}
}
ファクトリー関数:customResolver
export const customResolver = <_S extends Record<keyof _S, ValidationRules> = Record<string, ValidationRules>, _T extends ReactHookForm.FieldValues = Record<string, unknown>> (
schema: _S
): ReactHookForm.Resolver<_T> => {
~~~省略~~~
}
ファクトリー関数のパラメータ(カスタムリゾルバのオプション)
schema
- バリデーションルールを定義したオブジェクトを登録
React Hook Form とカスタムリゾルバの紐づけ
バリデーションルール
- バリデーションルールを定義
const loginIdValidationRule = {
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: '記号は @ - _ . のみ使用できます',
},
},
],
} as ValidationRules
useForm フックに登録
export function UserRegist() {
const sessionContext = SessionContext.useContext()
const reactHookForm = ReactHookForm.useForm<LocalUserRegistrationRequestObject>({
mode: 'onSubmit',
reValidateMode: 'onChange',
delayError: 200,
resolver: customResolver<Schema, LocalUserRegistrationRequestObject>({
loginId: loginIdValidationRule,
password: passwordValidationRule,
confirmPassword: confirmPasswordValidationRule,
nickName: nickNameValidationRule,
}),
defaultValues: {
loginId: '',
password: '',
confirmPassword: '',
nickName: '',
} as LocalUserRegistrationRequestObject,
})
React Hook Form の useForm フックのオプション
resolver
- カスタムリゾルバを登録
- カスタムリゾルバのオプション schema にバリデーションルールを定義したオブジェクトを登録
バリデーションを実施する関数
export const customResolver = <_S extends Record<keyof _S, ValidationRules> = Record<string, ValidationRules>, _T extends ReactHookForm.FieldValues = Record<string, unknown>> (
schema: _S
): ReactHookForm.Resolver<_T> => {
return async (objet, _, options) => {
~~~省略~~~
}
}
- カスタムリゾルバのファクトリー関数はバリデーションを実施する関数を返す
- React Hook Form は バリデーションを実施する関数を実行してバリデーションを実施
バリデーションを実施する関数のパラメータ
objet
- 各フィールドで入力した値は、 React Hook Form の useForm フック内部で入力値格納オブジェクトに集約
options
options.shouldUseNativeValidation
- useForm フックの shouldUseNativeValidation オプションに設定した値
options.criteriaMode
- useForm フックの criteriaMode オプションに設定した値
options.names
- バリデーション対象のフィールド名を格納した配列
- React Hook Form の register 関数の name プロパティに登録した値が格納される
-
onSubmit 時に格納される値(例)
names: 0: "loginId" 1: "password" 2: "confirmPassword" 3: "nickName"
-
onChange 時に格納される値(例)
names: 0: "loginId"
-
options.fields
- 格納される値は、React Hook Form 内部の実装に依存する
-
onSubmit 時に格納される値(例)
fields: loginId: {ref: input#ᆱr6ᄏ…, name: 'loginId', …} password: {ref: input#ᆱr7ᄏ-…, name: 'password', …} confirmPassword: {ref: input#ᆱr9ᄏ…, name: 'confirmPassword', …} nickName: {ref: input#ᆱrbᄏ…, name: 'nickName', …}
-
onChange 時に格納される値(例)
names: Array(4) loginId: {ref: input#ᆱr6ᄏ…, name: 'loginId', …}
-
入力値格納オブジェクトのキーと入力フィールドとの紐づけ
入力コンポーネントに登録
- 入力値格納オブジェクトのキーを入力コンポーネントに登録
<ReactHookForm.FormProvider {...reactHookForm}>
<TextFild<LocalUserRegistrationRequestObject>
name = {'loginId'}
~~~省略~~~
/>
<PasswordFild<LocalUserRegistrationRequestObject>
name = {'password'}
~~~省略~~~
/>
<PasswordFild<LocalUserRegistrationRequestObject>
name = {'confirmPassword'}
~~~省略~~~
/>
<TextFild<LocalUserRegistrationRequestObject>
name = {'nickName'}
~~~省略~~~
/>
</ReactHookForm.FormProvider>
入力コンポーネントのプロパティ
name
- 入力値格納オブジェクトのキーを登録
入力フィールドに登録
- React Hook Form の register 関数を使用して MUI TextField に登録
const registerProps = {
setValueAs: (value) => value.trim(),
} as StringRegisterOptions
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, registerProps)}
~~~省略~~~
/>
)
}
register 関数のプロパティ
name
- 入力値格納オブジェクトのキーを登録
バリデーション(onSubmit)
バリデーション実施
- handleSubmit 内で、カスタムリゾルバのバリデーションを実施する関数が実行される
バリデーション処理
- バリデーションを実施する関数内の処理
return async (objet, _, options) => {
const validateAllFieldCriteria = !options.shouldUseNativeValidation && options.criteriaMode === 'all'
const names = validateAllFieldCriteria
? buildNames(objet)
: options.names || []
~~~省略~~~
for (const name of names) {
const validationRules = getValidationRules(schema, name)
if (validationRules) {
const result = await validation({
value: getValue(objet, name),
rules: validationRules,
options: validationOptions,
commands: validationCommands,
})
if (result.length) {
errores[name] = result
}
}
}
~~~省略~~~
}
バリデーション対象
- バリデーション対象は、options.names に格納された値
names: 0: "loginId" 1: "password" 2: "confirmPassword" 3: "nickName"
バリデーションエラー有
- エラーオブジェクトの構造を React Hook Form の構造に変換
- サンプルでは、MUI の helperText で扱いやすい構造に変換
return async (objet, _, options) => {
~~~省略~~~
if (Object.keys(errores).length) {
return {
values: {},
errors: HookformResolvers.toNestErrors(
parseErrorSchema(errores, validateAllFieldCriteria),
options
),
}
}
~~~省略~~~
}
const parseErrorSchema = (errores: Record<string, string[]>, validateAllFieldCriteria: boolean): ReactHookForm.FieldErrors<Record<string, unknown>> => {
return Object.keys(errores).reduce<Record<string, ReactHookForm.FieldError>>((previous, path) => {
if (!previous[path]) {
previous[path] = {
message: errores[path].join('\n'),
type: '',
}
}
~~~省略~~~
return previous
}, {})
}
エラーメッセージ表示
- 入力コンポーネント内で 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}
/>
)
}
バリデーションエラー無
- 入力値格納オブジェクトの変換処理を実施
- React Hook Form の handleSubmit に登録した、submit コールバック関数のパラメータには、変換処理後の値が設定される
return async (objet, _, options) => {
~~~省略~~~
return {
values: Object.assign({}, objet),
errors: {},
}
}
バリデーション(onChange)
バリデーション実施
- onChange 内で、カスタムリゾルバのバリデーションを実施する関数が実行される
バリデーション処理
バリデーション対象
- バリデーション対象は、options.names に格納された値
- 入力操作を行った入力フィールドに紐づく React Hook Form の register 関数の name プロパティに登録した値が格納される(入力操作を行ったフィールドのバリデーションを実施)
names: 0: "loginId"
エラーメッセージ表示
- 入力コンポーネント内で formState を参照しエラーメッセージを表示
個別バリデーション
- パスワードとパスワード(確認用)の様に関連する入力フィールドが存在する場合は、個別のバリデーション処理を記述
パスワード(確認用)のバリデーション
バリデーションルール
- バリデーションルールにパスワードフィールドの入力値を参照する記述を追加
const confirmPasswordValidationRule = {
~~~省略~~~
isEquale: {
conditions: '%%password%%',
message: 'パスワードが異なっています',
},
} as ValidationRules
カスタムリゾルバ
- コールバック関数 ref を作成して、バリデーション処理のパラメータで渡す
return async (objet, _, options) => {
~~~省略~~~
const validationCommands: ValidationCommands = {
ref: (name) => {
return getValue(objet, name)
},
}
~~~省略~~~
const result = await validation({
value: getValue(objet, name),
rules: validationRules,
options: validationOptions,
commands: validationCommands,
})
~~~省略~~~
}
バリデーション処理
- コールバック関数 ref を実行して参照する値を取得
if (typeof isEqualeRule.conditions === 'string' && isEqualeRule.conditions.startsWith('%%') && isEqualeRule.conditions.endsWith('%%')) {
const refed = props.commands.ref(isEqualeRule.conditions.slice(2, -2).trim())
~~~省略~~~
}
パスワード入力時のバリデーション
- 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])
バリデーション対象
- options.names には、trigger 関数のパラメータに設定した 'confirmPassword' が格納される
names: 0: "confirmPassword"
ソースコード
- 利用者登録コンポーネント(サンプル)のソースコード
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 {ValidationRules} from '@/features/validator/rules/validation.rules'
import {MatchingPatterns} from '@/features/validator/patterns/matching.patterns'
import {customResolver} from '@/features/validator/react.hook.form/custom.resolver'
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
}
interface Schema {
loginId: ValidationRules
password: ValidationRules
confirmPassword: ValidationRules
nickName: ValidationRules
}
const loginIdValidationRule = {
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: '記号は @ - _ . のみ使用できます',
},
},
],
} as ValidationRules
const passwordValidationRule = {
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: '記号を含めてください',
},
},
],
} as ValidationRules
const confirmPasswordValidationRule = {
required: {
message: 'パスワード(確認用)を入力してください',
},
isEquale: {
conditions: '%%password%%',
message: 'パスワードが異なっています',
},
} as ValidationRules
const nickNameValidationRule = {
required: {
message: 'ニックネームを入力してください',
},
maxLength: {
conditions: MatchingPatterns.DB_WIDTH_VARCHAR_MAX,
message: `${MatchingPatterns.DB_WIDTH_VARCHAR_MAX}文字以内で入力してください`,
},
} as ValidationRules
export function UserRegist() {
const sessionContext = SessionContext.useContext()
const reactHookForm = ReactHookForm.useForm<LocalUserRegistrationRequestObject>({
mode: 'onSubmit',
reValidateMode: 'onChange',
delayError: 200,
resolver: customResolver<Schema, LocalUserRegistrationRequestObject>({
loginId: loginIdValidationRule,
password: passwordValidationRule,
confirmPassword: confirmPasswordValidationRule,
nickName: nickNameValidationRule,
}),
defaultValues: {
loginId: '',
password: '',
confirmPassword: '',
nickName: '',
} as LocalUserRegistrationRequestObject,
})
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): 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'}
/>
<PasswordFild<LocalUserRegistrationRequestObject>
name = {'password'}
label = {'パスワード'}
autoComplete = {'none'}
/>
<PasswordFild<LocalUserRegistrationRequestObject>
name = {'confirmPassword'}
label = {'パスワード(確認用)'}
autoComplete = {'none'}
/>
<TextFild<LocalUserRegistrationRequestObject>
name = {'nickName'}
label = {'ニックネーム'}
autoComplete = {'nickname'}
/>
<SubmitButton
label = {'登録'}
icon = {ICONS.AppRegistration}
/>
</ReactHookForm.FormProvider>
</UserFromLayout>
)
}
Discussion