概要
テキストボックスを拡張したコンポーネントをreact-hook-formで使えるようにするには一工夫必要です。registerをどうやって渡したらいいのか分からないという方も多いのではないでしょうか。試行錯誤してreact-hook-formに対応したカスタムテキストボックスを作ることができましたのでマイベストプラクティスとして共有させていただきたいと思います。
やり方
ジェネリクスを駆使してreact-hook-formに必要なパラメータを渡すようにします。今回はテキストボックスを作っていますが、ラジオボタンなど他のinputでも基本的な作りは変わらないと思います。
※以下ではuse-mask-inputを用いたマスク機能を持ち、バリデーションエラーメッセージを表示できるカスタムテキストボックスを作ります。CSSフレームワークとしてtailwind CSSを使っていますので、別のCSSフレームワークを使っている場合はCSSの箇所を変更してください。
TextBox.tsx
registerやerrorsなどreact-hook-formに必要なパラメータをPropsに定義します。型が難しいので真似て作ってください。
import { type HTMLInputTypeAttribute } from 'react'
import {
type UseFormRegister,
type FieldValues,
type FieldErrors,
type Path,
} from 'react-hook-form'
import { useHookFormMask } from 'use-mask-input'
type InputProps = React.ComponentProps<'input'>
type Props<T extends FieldValues> = InputProps & {
register: UseFormRegister<T>
errors: FieldErrors<T>
fieldName: Path<T> // instead of a string
type?: HTMLInputTypeAttribute
mask?: string // https://www.npmjs.com/package/use-mask-input
maskOption?: object
}
const TextBox = <T extends FieldValues>({
register,
errors,
fieldName,
type = 'text',
mask,
maskOption,
id,
...others
}: Props<T>) => {
const registerWithMask = useHookFormMask(register)
function createInput() {
const defaultClassName =
'mt-2 block w-full rounded-md border bg-white px-4 py-3 focus:border-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-40 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:focus:border-blue-300'
if (mask == null) {
return (
<input
type={type}
id={id ?? fieldName}
className={`${defaultClassName} ${getBorderColor()}`}
{...register(fieldName)}
{...others}
/>
)
} else {
return (
<input
type={type}
id={id ?? fieldName}
className={`${defaultClassName} ${getBorderColor()}`}
{...registerWithMask(fieldName, mask, maskOption)}
{...others}
/>
)
}
}
function getBorderColor() {
if (errors[fieldName] != null) {
return 'border-error'
}
return 'border-gray-200'
}
return (
<>
{createInput()}
{errors[fieldName]?.message != null && (
<p className="text-error">{String(errors[fieldName]?.message)}</p>
)}
</>
)
}
export default TextBox
使い方
registerやerrorsなどのパラメータを渡します。バリデーションのライブラリとしてはyupを使用していますが、他のライブラリでも動くと思います。
import { useForm, type SubmitHandler } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import TextBox from 'components/TextBox'
import * as yup from 'yup'
type FormValues = {
title: string
}
const informationSchema = yup.object().shape({
title: yup.string().required().max(30),
})
export default function FormWithValidation() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: yupResolver(informationSchema),
})
const onSubmit: SubmitHandler<FormValues> = (data) => {
console.log('submitData', data)
}
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<TextBox register={register} errors={errors} fieldName="title" />
</div>
<div>
<button type="submit">送信</button>
</div>
</form>
</div>
)
}