ReactHookFormでschemaを組み合わせてFormを作る
ReactHookForm
とyup
のschemaを組み合わせてフォームを作る方法です。
yup
と書きましたがzod
でも同じことは出来るはずです。
サンプルのコードはこちら
この記事ではReactHookForm
の基本的な使い方は知っている前提です。
ReactHookForm
とyupを組み合わせる
ReactHookForm
ではresolvers
を使うことでスキーマを元にバリデーションを行うことが出来ます
使い方はnpmリポジトリのQuickStartにある通り、useForm
のオプションに resolver: yupResolver(schema)
を与えてあげるだけです。
const { register, handleSubmit } = useForm({
resolver: yupResolver(schema),
});
schemaを分割する
Yup.object()
自体は展開できませんが、オブジェクトのフィールドをyup
で定義したスキーマにすることで、各スキーマを展開することで1つのスキーマを作ることが出来ます。
具体的には下記のようなコードです。
const userNameSchema = {
name: Yup.string().required('名前を入力してください')
}
const userEmailSchema = {
email: Yup.string().required('メールアドレスを入力してください')
}
const schema = Yup.object({
...userNameSchema,
...userEmailSchema
})
こうしてスキーマを分割しておくことで、スキーマを持ったそれぞれのコンポーネントを組み合わせてフォームを作ることが出来ます。
例えばあるフォームでは名前しかいらないときは、schemaには上記のuserNameSchema
だけ含めればいいといった具合です。
schemaから型を作る
yup
では定義したスキーマから型を作ることが出来ます
const userNameSchema = {
name: Yup.string().required('名前を入力してください')
}
const yupObject = Yup.object(userNameSchema)
type UserNameFormValues = Yup.InferType<typeof yupObject>
// => type UserNameFormValues = { name: string }
string
以外にもnumber
やboolean
,array
などが表現できます。
required
をつけないとstring | undefined
になります
ここで生成できる型とReactHookForm
を組み合わせて型の検証を生かしたフォームを作っていきます。
型からnameを作る
ReactHookForm
が提供するPath
型は、フォームの型を受け取り、有効なフィールド名のリテラル型を返してくれます。
import type { Path } from 'react-hook-form'
type NameType = Path<{ firstName: string; lastName: string; }>
// => type NameType = 'firstName' | 'lastName'
type UserNameType = Path<{ users: { firstName: string; lastName: string }[] }>
// => type UserNameType = "users" | `users.${number}` | `users.${number}.firstName` | `users.${number}.lastName`
配列やオブジェクトでもきちんとformに設定できるname
の型を返してくれます。
これを利用してinputのname
属性を型で検証できるようなコンポーネントを作ってみます。
TextInput
まずはコードを載せます。
import type { InputHTMLAttributes } from 'react'
import type { FieldValues, Path } from 'react-hook-form'
import { useFormContext } from 'react-hook-form'
export interface TextInputProps<T> extends Omit<InputHTMLAttributes<HTMLInputElement>, 'name'> {
name: Path<T>
}
export const TextInput = <T extends FieldValues = never>({
name,
...props
}: TextInputProps<T>): ReturnType<React.VFC> => {
const { register } = useFormContext()
return <input type="text" {...register(name)} {...props} />
}
先程のPath<T>
で作ったname
のみを受け取るようなTextInputコンポーネントです。
Path
とともにimportしているFieldValues
はuseForm
の型引数と同じ型で、実体はRecord<string, any>
です。デフォルトをnever
にすることで型を指定しないとエラーになるようにしています。
const userNameSchema = {
firstName: Yup.string().required('名前を入力してください')
lastName: Yup.string().required('姓を入力してください')
}
const yupObject = Yup.object(userNameSchema)
type UserNameFormValues = Yup.InferType<typeof yupObject>
export const UserName: React.VFC = () => {
return (
<>
<TextInput<UserNameFormValues> name="lastName">
<TextInput<UserNameFormValues> name="firstName">
</>
)
}
先程のname
のスキーマと組み合わせるとこんな感じになります。
与えた型に含まれるname
以外を入れるとエラーになり、name
のタイポを防ぐことが出来ます。
このようにyup
のスキーマとそこから生成される型とReactHookForm
が提供する型を組み合わせることで型の恩恵を受けたフォーム開発をすることが出来ます。
スキーマはコンポーネントごとに閉じているのでコンポーネント単位で組み合わせたりテストしたりすることが出来ます。
Discussion
バリデーションを伴うスキーマであれば、validateXXXのようにしてみても面白いかもです。
デモコードです。
簡単ですが、以上です。