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のようにしてみても面白いかもです。
デモコードです。
簡単ですが、以上です。