🦔
React Hook Form + zodで複雑な条件分岐を含んだschemaを書く
React Hook Formとzodを使えばバリデーションをいい感じに管理できるので最近使いはじめたのですが、○○がチェックされていたら△△は必須項目に、とか、チェックボックスが○個以上チェックされていたら別の項目を必須に...などなど、複雑な条件のSchemaを書く場合にハマったので備忘録を兼ねてポストします。
単純な条件の場合
unionを使う
項目にチェックが入っていたら、とかなら割と簡単でz.union()
とz.literal()
の組み合わせでユニオン型を作ることができる。
const selectSchema = z.union([
z.object({
ageInLaw: z.literal(false),
parentName: z
.string()
.max(30, '名前が長すぎます')
.optional(),
}),
z.object({
ageInLaw: z.literal(true),
parentName: z
.string()
.min(
1,
'未成年の場合は必ず入力してください'
)
.max(30, '名前が長すぎます'),
}),
])
もう少し複雑な場合
refine / superRefineを使う
literal
で表現できない条件の場合はrefine
を使って表現できる。
export const checkMovieCount = (
movies: { value: boolean }[] | undefined
) => {
return (
(movies &&
movies.filter((item) => item.value)
.length) ||
0
)
}
const checkboxSchema = z
.object({
favoriteMovie: z.array(
z.object({
value: z.boolean(),
})
),
reason: z
.string()
.max(200, '200文字以内で記入してください')
.optional(),
})
.refine(
({ favoriteMovie, reason }) =>
// 第二引数がfalseを返す条件の場合
!(
checkMovieCount(favoriteMovie) >= 3 &&
reason === ''
),
// 第3引数のバリデーションエラーを返す
{
path: ['reason'],
message:
'3つ以上選ばれた方は理由を記入してください',
}
)
ここでハマったのはrefine
の第二引数。ノリでここをtrue
になる条件で書いてて全然うまく反映されないと思ってたら、「false
の場合にバリデーションエラーが返る」だった。
この場合、元々理由欄がオプショナルで指定していたところを、チェックが3つ以上入ってて、かつ、理由欄に記入がなかったらエラーを返す=必須項目に変更する、という条件になる。
falseが欲しいので、「チェックが3つ以上入ってて、かつ、理由欄に記入がなかったら」を反転させている。
React Hook Formで実装
ほかにもいろいろスキーマを足してReact Hook Formに突っ込んだのがこちら
import { FC } from 'react'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import {
checkMovieCount,
schema,
Schema,
} from './schema'
const movies = [
{ label: 'ほげほげ', value: false },
{ label: 'ふがふが', value: false },
{ label: 'ほげほげ2', value: false },
{ label: 'ふがふが2', value: false },
{ label: 'ほげほげ3', value: false },
{ label: 'ふがふが3', value: false },
]
export const Form: FC = () => {
const {
register,
watch,
handleSubmit,
formState: { errors, isValid },
} = useForm<Schema>({
mode: 'onChange',
resolver: zodResolver(schema),
})
const movieWatcher = watch('favoriteMovie')
const onSubmit = (data: Schema) => {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<div>
<label htmlFor="name">
お名前
<input
type="text"
id="name"
placeholder="お名前"
{...register('name')}
/>
</label>
{errors.name && (
<p>{errors.name.message}</p>
)}
<label htmlFor="nickname">
ニックネーム
<input
type="text"
id="nickname"
placeholder="ニックネーム"
{...register('nickname')}
/>
</label>
{errors.nickname && (
<p>{errors.nickname.message}</p>
)}
</div>
<div>
<label htmlFor="ageInLaw">
未成年ですか?
<input
id="ageInLaw"
type="checkbox"
{...register('ageInLaw')}
/>
</label>
<label htmlFor="parentName">
両親の名前
<input
id="parentName"
type="text"
placeholder="両親の名前"
{...register('parentName')}
/>
</label>
{errors.parentName && (
<p>{errors.parentName.message}</p>
)}
</div>
<div>
{movies.map((movie, index) => (
<label key={index}>
{movie.label}
<input
type="checkbox"
{...register(
`favoriteMovie.${index}.value`
)}
/>
</label>
))}
</div>
{checkMovieCount(movieWatcher) >= 3 && (
<>
<label htmlFor="reason">
<input
type="text"
id="reason"
placeholder="理由"
{...register('reason')}
/>
</label>
{errors.reason && (
<p>{errors.reason.message}</p>
)}
</>
)}
<button type="submit" disabled={!isValid}>
Submit
</button>
</div>
</form>
)
}
resolver
をzodResolver
にしたところ以外はよく見るやつ。
バリデーションが外形化してるのでスッキリしていい感じになった。
多分チェックボックスをmapで回してるところはuseFieldArrayを使ったほうがいいんだろうな、なんて思いながらちゃんと調べるの時間かかりそうなので一旦これにて。
(スタイルは何もあててません。)
Discussion