【2022年】 React Hook FormでValidationライブラリはどれにするか?
React + Typescript + MUIv5 + React Hook Form で開発環境を作成しています。
今回は React Hook FormでValidationライブラリをどれにするか? について調査しました。
React Hook Form が標準で対応しているライブラリ
React Hook Form が標準で対応しているライブラリは以下の10個になります。
正確には React Hook Form で 外部の検証ライブラリを使用する為に @hookform/resolvers が必要なのですが、この @hookform/resolvers が標準対応しているライブラリが以下の10個になります。
その他のライブラリもカスタムリゾルバを構築して使用できます。
- class validator
https://github.com/typestack/class-validator - computed-types
https://github.com/neuledge/computed-types - io-ts
https://github.com/gcanti/io-ts - joi
https://github.com/sideway/joi - nope
https://www.npmjs.com/package/nope - superstruct
https://github.com/ianstormtaylor/superstruct - typanion
https://github.com/arcanis/typanion - vest
https://github.com/ealush/vest - yup
https://github.com/jquense/yup - zod
https://github.com/colinhacks/zod
■ npm人気順
npm の人気順です。
■ GitStarの順
GitStarの順です。
- joi(18,441)
- yup(16,342)
- class-validator(6,809)
- zod(6,705)
- superstruct(5,7488)
- io-ts(4,836)
- vest(1,816)
- computed-types(271)
- typanion(159)
■ 作成年度順
2020年:zod, typanion, computed-types
2018年:superstruct
2017年:io-ts
2016年:class-validato
2015年:vest, yup
2012年: joi
2011年:nope
■ ファイルサイズの小さい順
- nope(1.2KB)
- superstruct(3.4KB)
- computed-types(3.9KB)
- typanion(4.4KB)
- io-ts(5.2KB)
- vest(7.8KB)
- zod(10.4KB)
- yup(18.2KB)
- joi(42.0KB)
- class-validator(55.6KB)
GitStar上位の6つから選定する
たくさんあるので、GitStart順で上位の6つから選定することにします。
- joi(18,441)
- yup(16,342)
- class-validator(6,809)
- zod(6,705)
- superstruct(5,7488)
- io-ts(4,836)
■ joi
npmのダウンロード数、GitStarの数でも最も人気です。
ファイルサイズは42.0KBと大きいです。
サーバーサイドスクリプトを念頭に置いて設計されていて、フロントサイドではjoi-browserを使用します。
スキーマからTypeScriptの型を自動的に作成することができません。
フロントサイドだけで使用するなら、他のライブラリを使用した方がよいでしょう。
■ class-validator
ファイルサイズは55.6KBと大きいです。
フロントサイドだけで使用するなら、他のライブラリを使用した方がよいでしょう。
■ io-ts
ファイルサイズが5.2KBと小さいです。
io-tsは関数型プログラミングライブラリ「fp-ts」の作成者が作成したライブラリです。
fp-ts と io-ts は依存関係にあり、io-ts を使用するには fp-ts と関数型プログラミングの概念をある程度理解していないと使いこなせないようです。
■ yup
ファイルサイズは18.2KBです。
joiに大きく影響を受けていて、豊富なAPIがあります。
スキーマからTypeScriptの型を自動的に作成できます。
const schema = yup.object({
name: yup.string()
.required('名前を入力してください。')
.min(4, '4文字以上で入力してください。'),
})
コードは直観的でとても書きやすいです。
日本語情報も豊富で、迷っても解決策がすぐ見つかります。
デメリットとして、yup はスキーマからの型推論が zod に比べ弱いです。
また、yupはデフォルトですべてのオブジェクトの値をoptionalとしているため、スキーマ定義が冗長になります。
例えば、yupはデフォルトですべてのオブジェクトの値をoptionalとしているため、スキーマname: yup.string()
から推論される型はname: string | undefined
になります。
(zodの場合はname?: string | undefined
と省略可能プロパティになります)
// スキーマ
const schema = yup.object({
name: yup.string()
})
// 型推論で name: string | undefined になる
type Inputs = yup.InferType<typeof schema>
undefinedを許可しない場合はスキーマにrequired()
を指定します。
スキーマname: yup.string().required()
から推論される型はname: string
になります。
// スキーマ
const schema = yup.object({
name: yup.string().required()
})
// 型推論で name: stringになる
type Inputs = yup.InferType<typeof schema>
少し前の記事では、型推論に不具合がありオブジェクトのoptionalな値から生成された型がrequiredになってしまうということがあったようです。
つまり、スキーマname: yup.string()
はデフォルトでoptionalなので、推論される型がstring|undefined
になるべきところstring
になっているという不具合があったようです。
現在はスキーマname: yup.string()
の型推論はstring|undefined
となります。
ただし、nameプロパティは省略可能なプロパティにはなりません。
配列のスキーマで型推論をする例です。
スキーマyup.array().of(yup.boolean())
からの型推論は(boolean | undefined)[] | undefined
になります。
// スキーマ
const schema = yup.object({
checks: yup.array().of(yup.boolean())
})
// 型推論で checks: (boolean | undefined)[] | undefined になる
type Inputs = yup.InferType<typeof schema>
型boolean[]
を推論させるスキーマは
checks: yup.array().of(yup.boolean().required()).required()
となり冗長になります。
(後記のzodは非常にシンプルに書けます)
// スキーマ
const schema = yup.object({
checks: yup.array().of(yup.boolean().required()).required()
})
// 型推論で checks: (boolean | undefined)[] | undefined になる
type Inputs = yup.InferType<typeof schema>
また、yup ではstring | number
といった ユニオン型や、インターセクション型が表現できません。(zodでは表現できます)
React(ts) + ReactHookForm + MUIv5 TextField + yup のサンプルコード
インストール
npm install @hookform/resolvers
npm install yup
React(ts) + ReactHookForm + MUIv5 TextField のサンプルコード
import { Stack, TextField, Button } from '@mui/material'
import { useForm, SubmitHandler, Controller } from 'react-hook-form'
+ import { yupResolver } from '@hookform/resolvers/yup'
+ import * as yup from "yup"
// yupを使用しない場合
- // type Inputs = {
- // name: string
- // }
- // const validationRules = {
- // name: {
- // required: '名前を入力してください。',
- // minLength: { value: 4, message: '4文字以上で入力してください。' }
- // }
- // }
// yupを使用する場合
+ const schema = yup.object({
+ name: yup.string()
+ .required('名前を入力してください。')
+ .min(4, '4文字以上で入力してください。'),
+ })
+ type Inputs = yup.InferType<typeof schema>
export function InputReactHookFormTextField() {
const {
control,
handleSubmit
} = useForm<Inputs>({
defaultValues: { name: 'piyoko' },
+ resolver: yupResolver(schema) // yupを使用する場合は指定する
})
const onSubmit: SubmitHandler<Inputs> = (data: Inputs) => {
console.log(`submit: ${data.name} typeof: ${typeof data.name}`)
}
return (
<Stack component="form" noValidate
onSubmit={handleSubmit(onSubmit)}
spacing={2} sx={{ m: 2, width: '25ch' }}>
<Controller
name="name"
control={control}
- // rules={validationRules.name} // yup を使用しない場合は必要
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
label="名前"
error={fieldState.invalid}
helperText={fieldState.error?.message}
/>
)}
/>
<Button variant="contained" type="submit" >
送信する
</Button>
</Stack>
)
}
■ zod
ファイルサイズは10.4KBです。
Blitzにも使われている注目のライブラリです。
(Blitzは、Reactのライブラリの選定といった面倒なことを全部引き受けてくれるフルスタックフレームワークです)
最も後発のライブラリということもあり、他のライブラリの良い点・悪い点を参考にしているので優位です。
Zodでは値を「検証」ではなく「パース」しています。
パースに成功するとパース後の値を返し、失敗した場合はエラーをスローします。
スキーマからTypeScriptの型を自動的に作成でき、その型推論は正確です。
Zodはデフォルトですべての検証が必要と想定されていて、不要な場合は.optional()
を指定します。
つまり、スキーマname: yup.string()
から推論される型はname: string
になります。
.optional()
を指定したスキーマname: yup.string().optional()
から推論される型はname?: string | undefined
になり省略可能なプロパティとなります。
// スキーマ
const schema = z.object({
firstname: z.string(),
lastname: z.string().optional()
})
// 型推論で firstname: stringに、lastname?:string | undefined になる
type Inputs = z.infer<typeof schema>
また、zodではstring | number
といった ユニオン型や、インターセクション型が表現できます。
たとえば、スキーマname: z.union([z.string(), z.number()])
から推論される方はユニオン型のname: string | number
となります。
型推論については yup より優れているといえます。
// スキーマ
const schema = z.object({
name: z.union([z.string(), z.number()])
})
// 型推論で name:string | number になる
type Inputs = z.infer<typeof schema>
配列のスキーマで型推論をする例です。
スキーマz.array()(z.boolean())
からの型推論はboolean[]
になります。
yupに比べシンプルに書けます。
// スキーマ
const schema = z.object({
checks: z.array(z.boolean())
})
// 型推論で checks: boolean[] になる
type Inputs = z.infer<typeof schema>
React(ts) + ReactHookForm + MUIv5 TextField + zod のサンプルコード
インストール
npm install @hookform/resolvers
npm install zod
React(ts) + ReactHookForm + MUIv5 TextField のサンプルコード
import { Stack, TextField, Button } from '@mui/material'
import { useForm, SubmitHandler, Controller } from 'react-hook-form'
+ import { zodResolver } from '@hookform/resolvers/zod'
+ import { z } from "zod"
// zodを使用しない場合
- // type Inputs = {
- // name: string
- // }
- // const validationRules = {
- // name: {
- // required: '名前を入力してください。',
- // minLength: { value: 4, message: '4文字以上で入力してください。' }
- // }
- // }
// zodを使用する場合
+ const schema = z.object({
+ name: z.string()
+ .nonempty('名前を入力してください。-zod')
+ .min(4, '4文字以上で入力してください。-zod')
+ })
+ type Inputs = z.infer<typeof schema>
export function InputReactHookFormTextField() {
const {
control,
handleSubmit
} = useForm<Inputs>({
defaultValues: { name: 'piyoko' },
+ resolver: zodResolver(schema) // zodを使用する場合は指定する
})
const onSubmit: SubmitHandler<Inputs> = (data: Inputs) => {
console.log(`submit: ${data.name}`)
}
return (
<Stack component="form" noValidate
onSubmit={handleSubmit(onSubmit)}
spacing={2} sx={{ m: 2, width: '25ch' }}>
{/* コントローラー */}
<Controller
name="name"
control={control}
- // rules={validationRules.name} // zod を使用しない場合は必要
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
label="名前"
error={fieldState.invalid}
helperText={fieldState.error?.message}
/>
)}
/>
<Button variant="contained" type="submit" >
送信する
</Button>
</Stack>
)
}
■ superstruct
ファイルサイズが3.4KBと小さいです。
依存関係はなく、バックエンドとフロントエンドの両方で使用できます。
日本語情報はかなり少ないです。
書き方はチェーンではなく、関数型でネストするように書きます。
スキーマからTypeScriptの型を自動的に作成できます。
superstructは構造体でデータを定義します。
以下のようなスキーマを定義すると、nameは ss.Struct<string, null>
といった構造体になります。
const schema = ss.type({
name: ss.string()
})
superstructはデフォルトですべての検証が必要と想定されています。
つまり、スキーマname: yup.string()
から推論される型はname: string
になります。
// スキーマ
const schema = ss.type({
name: ss.string()
})
// 型推論 name:string
type Inputs = ss.Infer<typeof schema>
値がoptionalな場合は、下記のコードのように構造体をoptional()でラップします。
推論される型はname?: string | undefined
になり省略可能なプロパティとなります。
// スキーマ
const schema = ss.type({
name: ss.optional(ss.string())
})
// 型推論 name?: string | undefined
type Inputs = ss.Infer<typeof schema>
name構造体の値が、optional かつ 文字数が4文字以上とする場合、下記のように構造体を size() でラップします。
const schema = ss.type({
name: ss.optional(ss.size(ss.string(), 4))
})
さらに name はアルファベットのみとする場合は、さらに構造体をpattern()でラップします。
const schema = ss.type({
name: ss.optional(ss.pattern(ss.size(ss.string(), 4), /[a-zA-Z]/))
})
さらに検証が続くと・・・ラップしてラップして・・・superstruct の書き方は好みが分かれるのではないでしょうか。
yup か zod か どちらにするか?
個人的な好みで、最終的に yup と zod が選択肢に残りました。
バックエンドでも検証を使用するなら zod に利点がありそうです。
フロントエンドだけの使用の場合だとどうでしょうか。
以前、コチラの記事でReactHookFormの機能だけで Validation を実装しました。
MUI v5 + React Hook Form v7 で、よく使うコンポーネント達を連携してみる
この記事で実装した Validation を yup と zod で書き比べてみました。
yup と zod で書き比べしたコンポーネントは、下記の5種になります。
- TextField
- Select
- RadioGroup
- DatePicker
- 複数のチェックボックス
■ yup vs zod サンプルコード
React(ts) + ReactHookForm + MUIv5 TextField
import { Stack, TextField, Button } from '@mui/material'
import { useForm, SubmitHandler, Controller } from 'react-hook-form'
+ import { yupResolver } from '@hookform/resolvers/yup'
+ import * as yup from "yup"
// yupを使用しない場合
- // type Inputs = {
- // name: string
- // }
- // const validationRules = {
- // name: {
- // required: '名前を入力してください。',
- // minLength: { value: 4, message: '4文字以上で入力してください。' }
- // }
- // }
// yupを使用する場合
+ const schema = yup.object({
+ name: yup.string()
+ .required('名前を入力してください。')
+ .min(4, '4文字以上で入力してください。'),
+ })
type Inputs = yup.InferType<typeof schema>
export function InputReactHookFormTextField() {
const {
control,
handleSubmit
} = useForm<Inputs>({
defaultValues: { name: 'piyoko' },
+ resolver: yupResolver(schema) // yupを使用する場合は指定する
})
const onSubmit: SubmitHandler<Inputs> = (data: Inputs) => {
console.log(`submit: ${data.name} typeof: ${typeof data.name}`)
}
return (
<Stack component="form" noValidate
onSubmit={handleSubmit(onSubmit)}
spacing={2} sx={{ m: 2, width: '25ch' }}>
<Controller
name="name"
control={control}
- // rules={validationRules.name} // yup を使用しない場合は必要
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
label="名前"
error={fieldState.invalid}
helperText={fieldState.error?.message}
/>
)}
/>
<Button variant="contained" type="submit" >
送信する
</Button>
</Stack>
)
}
import { Stack, TextField, Button } from '@mui/material'
import { useForm, SubmitHandler, Controller } from 'react-hook-form'
+ import { zodResolver } from '@hookform/resolvers/zod'
+ import { z } from "zod"
// zodを使用しない場合
- // type Inputs = {
- // name: string
- // }
- // const validationRules = {
- // name: {
- // required: '名前を入力してください。',
- // minLength: { value: 4, message: '4文字以上で入力してください。' }
- // }
- // }
// zodを使用する場合
+ const schema = z.object({
+ name: z.string()
+ .nonempty('名前を入力してください。')
+ .min(4, '4文字以上で入力してください。')
+ })
+ type Inputs = z.infer<typeof schema>
export function InputReactHookFormTextField() {
const {
control,
handleSubmit
} = useForm<Inputs>({
defaultValues: { name: 'piyoko' },
+ resolver: zodResolver(schema) // zodを使用する場合は指定する
})
const onSubmit: SubmitHandler<Inputs> = (data: Inputs) => {
console.log(`submit: ${data.name}`)
}
return (
<Stack component="form" noValidate
onSubmit={handleSubmit(onSubmit)}
spacing={2} sx={{ m: 2, width: '25ch' }}>
{/* コントローラー */}
<Controller
name="name"
control={control}
- // rules={validationRules.name} // zod を使用しない場合は必要
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
label="名前"
error={fieldState.invalid}
helperText={fieldState.error?.message}
/>
)}
/>
<Button variant="contained" type="submit" >
送信する
</Button>
</Stack>
)
}
React(ts) + ReactHookForm + MUIv5 Select
import { Stack, FormControl, InputLabel, Select, MenuItem, FormHelperText, Button } from '@mui/material'
import { useForm, SubmitHandler, Controller } from 'react-hook-form'
+ import { yupResolver } from '@hookform/resolvers/yup'
+ import * as yup from "yup"
- // type Inputs = {
- // area: number | ''
- // }
- // const validationRules = {
- // area: {
- // validate: (value:number | '') => value !== '' || 'いずれかを選択してください。'
- // }
- // }
+ const schema = yup.object({
+ area: yup.number()
+ .transform((value, originalvalue) => originalvalue === '' ? undefined : value)
+ .required('いずれかを選択してください。')
+ })
+ type Inputs = yup.InferType<typeof schema>
export function InputReactHookFormSelect() {
const {
control,
handleSubmit
} = useForm<Inputs>({
defaultValues: { area: 6 },
+ resolver: yupResolver(schema)
})
const onSubmit: SubmitHandler<Inputs> = (data: Inputs) => {
console.log(`submit: ${data.area} typeof: ${typeof data.area}`)
}
return (
<Stack component="form" noValidate
onSubmit={handleSubmit(onSubmit)}
spacing={2}
sx={{ m: 2, width: '25ch' }}>
<Controller
name="area"
control={control}
- // rules={validationRules.area}
render={({ field, fieldState }) => (
<FormControl fullWidth error={fieldState.invalid}>
<InputLabel id="area-label">地域</InputLabel>
<Select
labelId="area-label"
label=" " // フォーカスを外した時のラベルの部分こ(これを指定しないとラベルとコントロール線が被る)
{...field}
>
<MenuItem value='' sx={{color:'gray'}}>未選択</MenuItem>
<MenuItem value={1}>北海道</MenuItem>
<MenuItem value={2}>東北</MenuItem>
<MenuItem value={4}>関東</MenuItem>
<MenuItem value={5}>中部</MenuItem>
<MenuItem value={6}>近畿</MenuItem>
<MenuItem value={7}>中国</MenuItem>
<MenuItem value={8}>四国</MenuItem>
<MenuItem value={9}>九州沖縄</MenuItem>
</Select>
<FormHelperText>{fieldState.error?.message}</FormHelperText>
</FormControl>
)}
/>
<Button variant="contained" type="submit" >
送信する
</Button>
</Stack>
)
}
/* eslint-disable react/jsx-props-no-spreading */
import { Stack, FormControl, InputLabel, Select, MenuItem, FormHelperText, Button } from '@mui/material'
import { useForm, SubmitHandler, Controller } from 'react-hook-form'
+ import { zodResolver } from '@hookform/resolvers/zod'
+ import { z } from "zod"
- // type Inputs = {
- // area: number | ''
- // }
- // const validationRules = {
- // area: {
- // validate: (value:number | '') => value !== '' || 'いずれかを選択してください。'
- // }
- // }
+ const schema = z.object({
+ area: z.union([z.string(), z.number()])
+ .refine(
+ (val) => val !== '', { message: 'いずれかを選択してください。' }
+ )
+ })
+ type Inputs = z.infer<typeof schema>
export function InputReactHookFormSelect() {
const {
control,
handleSubmit
} = useForm<Inputs>({
defaultValues: { area: 6 },
+ resolver: zodResolver(schema)
})
const onSubmit: SubmitHandler<Inputs> = (data: Inputs) => {
console.log(`submit: ${data.area}`)
}
return (
<Stack component="form" noValidate
onSubmit={handleSubmit(onSubmit)}
spacing={2}
sx={{ m: 2, width: '25ch' }}>
<Controller
name="area"
control={control}
- // rules={validationRules.area}
render={({ field, fieldState }) => (
<FormControl fullWidth error={fieldState.invalid}>
<InputLabel id="area-label">地域</InputLabel>
<Select
labelId="area-label"
label=" " // フォーカスを外した時のラベルの部分こ(これを指定しないとラベルとコントロール線が被る)
{...field}
>
<MenuItem value='' sx={{color:'gray'}}>未選択</MenuItem>
<MenuItem value={1}>北海道</MenuItem>
<MenuItem value={2}>東北</MenuItem>
<MenuItem value={4}>関東</MenuItem>
<MenuItem value={5}>中部</MenuItem>
<MenuItem value={6}>近畿</MenuItem>
<MenuItem value={7}>中国</MenuItem>
<MenuItem value={8}>四国</MenuItem>
<MenuItem value={9}>九州沖縄</MenuItem>
</Select>
<FormHelperText>{fieldState.error?.message}</FormHelperText>
</FormControl>
)}
/>
<Button variant="contained" type="submit" >
送信する
</Button>
</Stack>
)
}
React(ts) + ReactHookForm + MUIv5 RadioGroup
import {
Stack,
RadioGroup,
FormLabel,
FormControlLabel,
Radio,
FormControl,
Button,
FormHelperText
} from '@mui/material'
import { useForm, SubmitHandler, Controller } from 'react-hook-form'
+ import { yupResolver } from '@hookform/resolvers/yup'
+ import * as yup from "yup"
- // type Inputs = {
- // gender: number
- // }
- // const validationRules = {
- // gender: {
- // validate: (value: number) => value !== -1 || 'いずれかを選択してください。'
- // }
- // }
+ const schema = yup.object({
+ gender: yup.number().moreThan(-1,'いずれかを選択してください。')
+ })
+ type Inputs = yup.InferType<typeof schema>
export function InputReactHookFormRadioGroup() {
const {
control,
handleSubmit
} = useForm<Inputs>({
defaultValues: { gender: -1 },
+ resolver: yupResolver(schema)
})
const onSubmit: SubmitHandler<Inputs> = (data: Inputs) => {
console.log(`submit: ${data.gender} typeof: ${typeof data.gender}`)
}
return (
<Stack component="form" noValidate onSubmit={handleSubmit(onSubmit)} spacing={2} sx={{ m: 2, width: '25ch' }}>
{/* 6.Controllerコンポーネントで TextFieldをReactHookFormと紐づけます。 */}
<Controller
name="gender"
control={control}
- // rules={validationRules.gender}
render={({ field, fieldState }) => (
<FormControl error={fieldState.invalid}>
<FormLabel id="radio-buttons-group-label">Gender</FormLabel>
<RadioGroup
aria-labelledby="radio-buttons-group-label"
value={field.value} name="gender">
<FormControlLabel {...field} value={1} control={<Radio />} label="男性" />
<FormControlLabel {...field} value={2} control={<Radio />} label="女性" />
<FormControlLabel {...field} value={0} control={<Radio />} label="未回答" />
</RadioGroup>
<FormHelperText>{fieldState.error?.message}</FormHelperText>
</FormControl>
)}
/>
<Button variant="contained" type="submit">
送信する
</Button>
</Stack>
)
}
import {
Stack,
RadioGroup,
FormLabel,
FormControlLabel,
Radio,
FormControl,
Button,
FormHelperText
} from '@mui/material'
import { useForm, SubmitHandler, Controller } from 'react-hook-form'
+ import { zodResolver } from '@hookform/resolvers/zod'
+ import { z } from "zod"
- // type Inputs = {
- // gender: number
- // }
- // const validationRules = {
- // gender: {
- // validate: (value: number) => value !== -1 || 'いずれかを選択してください。'
- // }
- // }
+ const schema = z.object({
+ gender: z.number()
+ .or(z.string().transform(Number))
+ .refine(
+ (val) => val > 0, 'いずれかを選択してください。')
+ })
+ type Inputs = z.infer<typeof schema>
export function InputReactHookFormRadioGroup() {
const {
control,
handleSubmit,
} = useForm<Inputs>({
defaultValues: { gender: -1 },
+ resolver: zodResolver(schema)
})
const onSubmit: SubmitHandler<Inputs> = (data: Inputs) => {
console.log(`submit: ${data.gender} typeof ${typeof data.gender}`)
}
return (
<Stack component="form" noValidate onSubmit={handleSubmit(onSubmit)} spacing={2} sx={{ m: 2, width: '25ch' }}>
<Controller
name="gender"
control={control}
- // rules={validationRules.gender}
render={({ field, fieldState }) => (
<FormControl error={fieldState.invalid}>
<FormLabel id="radio-buttons-group-label">Gender</FormLabel>
<RadioGroup
aria-labelledby="radio-buttons-group-label"
value={field.value} name="gender">
<FormControlLabel {...field} value={1} control={<Radio />} label="男性" />
<FormControlLabel {...field} value={2} control={<Radio />} label="女性" />
<FormControlLabel {...field} value={3} control={<Radio />} label="未回答" />
</RadioGroup>
<FormHelperText>{fieldState.error?.message}</FormHelperText>
</FormControl>
)}
/>
<Button variant="contained" type="submit">
送信する
</Button>
</Stack>
)
}
React(ts) + ReactHookForm + MUIv5 DatePicker
import * as React from 'react'
import { Stack, TextField, Button } from '@mui/material'
import { useForm, SubmitHandler, Controller } from 'react-hook-form'
import { LocalizationProvider, DatePicker } from '@mui/lab'
import AdapterDateFns from '@mui/lab/AdapterDateFns'
import ja from 'date-fns/locale/ja'
+ import { yupResolver } from '@hookform/resolvers/yup'
+ import * as yup from "yup"
- // type Inputs = {
- // applicationDate: Date | null
- // }
- // const validationRules = {
- // applicationDate: {
- // validate: (val: Date | null) => {
- // if (val == null) {
- // return '申請日を入力してください。'
- // }
- // if (Number.isNaN(val.getTime())) {
- // return '日付を正しく入力してください。'
- // }
- // return true
- // }
- // }
- // }
+ const schema = yup.object({
+ applicationDate: yup.date()
+ .transform((value, originalvalue) => originalvalue == null ? undefined : value)
+ .required('申請日を入力してください。')
+ .typeError('日付を正しく入力してください。')
+ })
+ type Inputs = yup.InferType<typeof schema>
export function InputDateTimePicker() {
const {
control,
handleSubmit,
} = useForm<Inputs>({
defaultValues: { applicationDate: new Date() },
+ resolver: yupResolver(schema)
})
const onSubmit: SubmitHandler<Inputs> = (data: Inputs) => {
console.log(`submit: ${data.applicationDate} typeof: ${typeof data.applicationDate}`)
}
return (
<LocalizationProvider dateAdapter={AdapterDateFns} locale={ja}>
<Stack component="form" noValidate onSubmit={handleSubmit(onSubmit)} spacing={2} sx={{ m: 2, width: '25ch' }}>
<Controller
name="applicationDate"
control={control}
- // rules={validationRules.applicationDate}
render={({ field, fieldState }) => (
<DatePicker
label="申請日"
inputFormat="yyyy年MM月dd日"
mask="____年__月__日"
leftArrowButtonText="前月を表示"
rightArrowButtonText="次月を表示"
toolbarTitle="日付選択"
cancelText="キャンセル"
okText="選択"
toolbarFormat="yyyy年MM月dd日"
renderInput={(params) => (
<TextField
{...params}
error={fieldState.invalid}
helperText={fieldState.invalid ? fieldState?.error?.message : null}
/>
)}
PaperProps={{ sx: styles.paperprops }}
DialogProps={{ sx: styles.mobiledialogprops }}
{...field}
/>
)}
/>
<Button variant="contained" type="submit">
送信する
</Button>
</Stack>
</LocalizationProvider>
)
}
const styles = {
componentsProps: {
color: 'green'
},
paperprops: {
'div[role=presentation]': {
display: 'flex',
'& .PrivatePickersFadeTransitionGroup-root:first-of-type': {
order: 2
},
'& .PrivatePickersFadeTransitionGroup-root:nth-of-type(2)': {
order: 1,
'& div::after': {
content: '"年"'
}
},
'& .MuiButtonBase-root': {
order: 3
}
}
},
mobiledialogprops: {
'.PrivatePickersToolbar-dateTitleContainer .MuiTypography-root': {
fontSize: '1.5rem'
},
'div[role=presentation]:first-of-type': {
display: 'flex',
'& .PrivatePickersFadeTransitionGroup-root:first-of-type': {
order: 2
},
'& .PrivatePickersFadeTransitionGroup-root:nth-of-type(2)': {
order: 1,
'& > div::after': {
content: '"年"'
}
},
'& .MuiButtonBase-root': {
order: 3
}
}
}
}
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable @typescript-eslint/no-use-before-define */
import * as React from 'react'
import { Stack, TextField, Button } from '@mui/material'
import { useForm, SubmitHandler, Controller } from 'react-hook-form'
import { LocalizationProvider, DatePicker } from '@mui/lab'
import AdapterDateFns from '@mui/lab/AdapterDateFns'
import ja from 'date-fns/locale/ja'
+ import { zodResolver } from '@hookform/resolvers/zod'
+ import { z } from "zod"
- // type Inputs = {
- // applicationDate: Date | null
- // }
- // const validationRules = {
- // applicationDate: {
- // validate: (val: Date | null) => {
- // if (val == null) {
- // return '申請日を入力してください。'
- // }
- // if (Number.isNaN(val.getTime())) {
- // return '日付を正しく入力してください。'
- // }
- // return true
- // }
- // }
- // }
+ const errMap: z.ZodErrorMap = (issue, _ctx) => {
+ let message
+ switch (issue.code) {
+ case z.ZodIssueCode.invalid_type:
+ message = `申請日を入力してください。`
+ break
+ case z.ZodIssueCode.invalid_date:
+ message = `日付を正しく入力してください。`
+ break
+ default:
+ message = _ctx.defaultError
+ }
+ return { message }
+ }
+ const schema = z.object({
+ applicationDate: z.date({errorMap: errMap})
+ })
+ type Inputs = z.infer<typeof schema>
export function InputDateTimePicker() {
const {
control,
handleSubmit,
} = useForm<Inputs>({
defaultValues: { applicationDate: new Date() },
+ resolver: zodResolver(schema)
})
const onSubmit: SubmitHandler<Inputs> = (data: Inputs) => {
console.log(`submit: ${data.applicationDate}`)
}
return (
<LocalizationProvider dateAdapter={AdapterDateFns} locale={ja}>
<Stack component="form" noValidate onSubmit={handleSubmit(onSubmit)} spacing={2} sx={{ m: 2, width: '25ch' }}>
<Controller
name="applicationDate"
control={control}
- // rules={validationRules.applicationDate}
render={({ field, fieldState }) => (
<DatePicker
label="申請日"
inputFormat="yyyy年MM月dd日"
mask="____年__月__日"
leftArrowButtonText="前月を表示"
rightArrowButtonText="次月を表示"
toolbarTitle="日付選択"
cancelText="キャンセル"
okText="選択"
toolbarFormat="yyyy年MM月dd日"
renderInput={(params) => (
<TextField
{...params}
error={fieldState.invalid}
helperText={fieldState.invalid ? fieldState?.error?.message : null}
/>
)}
PaperProps={{ sx: styles.paperprops }}
DialogProps={{ sx: styles.mobiledialogprops }}
{...field}
/>
)}
/>
<Button variant="contained" type="submit">
送信する
</Button>
</Stack>
</LocalizationProvider>
)
}
const styles = {
componentsProps: {
color: 'green'
},
paperprops: {
'div[role=presentation]': {
display: 'flex',
'& .PrivatePickersFadeTransitionGroup-root:first-of-type': {
order: 2
},
'& .PrivatePickersFadeTransitionGroup-root:nth-of-type(2)': {
order: 1,
'& div::after': {
content: '"年"'
}
},
'& .MuiButtonBase-root': {
order: 3
}
}
},
mobiledialogprops: {
'.PrivatePickersToolbar-dateTitleContainer .MuiTypography-root': {
fontSize: '1.5rem'
},
'div[role=presentation]:first-of-type': {
display: 'flex',
'& .PrivatePickersFadeTransitionGroup-root:first-of-type': {
order: 2
},
'& .PrivatePickersFadeTransitionGroup-root:nth-of-type(2)': {
order: 1,
'& > div::after': {
content: '"年"'
}
},
'& .MuiButtonBase-root': {
order: 3
}
}
}
}
React(ts) + ReactHookForm + MUIv5 複数のCheckBox
import { Stack, Button, FormControlLabel, Checkbox, FormControl, FormGroup, FormHelperText } from '@mui/material'
import { useForm, SubmitHandler, Controller } from 'react-hook-form'
+ import { yupResolver } from '@hookform/resolvers/yup'
+ import * as yup from 'yup'
- // type Inputs = {
- // checks: boolean[]
- // checkerr: boolean
- // }
- // const validationRules = {
- // checks: {
- // validate: () => {
- // clearErrors(`checkerr`)
- // const checks = [getValues(`checks.${0}`), getValues(`checks.${1}`), getValues(`checks.${2}`)]
- // if (checks.filter((v) => v === true).length < 2) {
- // setError(`checkerr`, { message: 'いずれか2つ選択してください。' })
- // }
- // return true
- // }
- // }
- // }
+ const schema = yup.object({
+ checks: yup.array().of(yup.boolean().required()).required(),
+ checkerr: yup.boolean().required().test('checklength', 'いずれか2つ選択してください。', function() {
+ return this.parent.checks.filter((v:boolean) => v === true).length >= 2
+ })
+ })
+ type Inputs = yup.InferType<typeof schema>
export function InputReactHookCheckBox() {
const {
control,
handleSubmit,
formState: { errors }
- // getValues,
- // clearErrors,
- // setError
} = useForm<Inputs>({
defaultValues: {
checks: [false, true, false],
checkerr: false
},
+ resolver: yupResolver(schema)
})
const onSubmit: SubmitHandler<Inputs> = (data: Inputs) => {
console.log(`submit: checks[0]=${data.checks[0]} checks[1]=${data.checks[1]} checks[2]=${data.checks[2]} typeof: ${typeof data.checks}`)
}
return (
<Stack component="form" noValidate onSubmit={handleSubmit(onSubmit)} spacing={2} sx={{ m: 2, width: '25ch' }}>
<FormControl fullWidth error={errors.checkerr !== undefined}>
<span>いずれか2つ選択。</span>
<FormGroup>
<Controller
name={`checks.${0}`}
control={control}
- // rules={validationRules.checks}
render={({ field }) => (
<FormControlLabel label="チェック 1" control={<Checkbox {...field} checked={field.value} />} />
)}
/>
<Controller
name={`checks.${1}`}
control={control}
// rules={validationRules.checks}
render={({ field }) => (
<FormControlLabel label="チェック 2" control={<Checkbox {...field} checked={field.value} />} />
)}
/>
<Controller
name={`checks.${2}`}
control={control}
// rules={validationRules.checks}
render={({ field }) => (
<FormControlLabel label="チェック 3" control={<Checkbox {...field} checked={field.value} />} />
)}
/>
</FormGroup>
<FormHelperText>{errors.checkerr?.message}</FormHelperText>
</FormControl>
<Button variant="contained" type="submit">
送信する
</Button>
</Stack>
)
}
import { Stack, Button, FormControlLabel, Checkbox, FormControl, FormGroup, FormHelperText } from '@mui/material'
import { useForm, SubmitHandler, Controller } from 'react-hook-form'
+ import { zodResolver } from '@hookform/resolvers/zod'
+ import { z } from "zod"
- // type Inputs = {
- // checks: boolean[]
- // checkerr: boolean
- // }
- // const validationRules = {
- // checks: {
- // validate: () => {
- // clearErrors(`checkerr`)
- // const checks = [getValues(`checks.${0}`), getValues(`checks.${1}`), getValues(`checks.${2}`)]
- // if (checks.filter((v) => v === true).length < 2) {
- // setError(`checkerr`, { message: 'いずれか2つ選択してください。' })
- // }
- // return true
- // }
- // }
- // }
+ const schema = z.object({
+ checks: z.array(z.boolean()),
+ checkerr: z.boolean()
+ })
+ .refine(data => data.checks.filter((v) => v === true).length >= 2, {
+ message: 'いずれか2つ選択してください。',
+ path: ['checkerr'],
+ })
+ type Inputs = z.infer<typeof schema>
export function InputReactHookCheckBox() {
const {
control,
handleSubmit,
formState: { errors }
- // getValues,
- // clearErrors,
- // setError
} = useForm<Inputs>({
defaultValues: {
checks: [false, true, false],
checkerr: false
},
+ resolver: zodResolver(schema)
})
const onSubmit: SubmitHandler<Inputs> = (data: Inputs) => {
console.log(`submit: checks[0]=${data.checks[0]} checks[1]=${data.checks[1]} checks[2]=${data.checks[2]} typeof: ${typeof data.checks}`)
}
return (
<Stack component="form" noValidate onSubmit={handleSubmit(onSubmit)} spacing={2} sx={{ m: 2, width: '25ch' }}>
<FormControl fullWidth error={errors.checkerr !== undefined}>
<span>いずれか2つ選択。</span>
<FormGroup>
<Controller
name={`checks.${0}`}
control={control}
- // rules={validationRules.checks}
render={({ field }) => (
<FormControlLabel label="チェック 1" control={<Checkbox {...field} checked={field.value} />} />
)}
/>
<Controller
name={`checks.${1}`}
control={control}
// rules={validationRules.checks}
render={({ field }) => (
<FormControlLabel label="チェック 2" control={<Checkbox {...field} checked={field.value} />} />
)}
/>
<Controller
name={`checks.${2}`}
control={control}
// rules={validationRules.checks}
render={({ field }) => (
<FormControlLabel label="チェック 3" control={<Checkbox {...field} checked={field.value} />} />
)}
/>
</FormGroup>
<FormHelperText>{errors.checkerr?.message}</FormHelperText>
</FormControl>
<Button variant="contained" type="submit">
送信する
</Button>
</Stack>
)
}
■ 書き比べた結果
型推論はやはり zod が優位です。
個人的にはスキーマ定義の見やすさは yup のほうがわかりやすいのではと思います。
分かりやすさを見るために、上記で書き比べた yup と zod のサンプルコードから スキーマ定義部分だけを抜き出しました。
TextField
TextFieldではあまり違いは感じません。
const schema = yup.object({
name: yup.string()
.required('名前を入力してください。')
.min(4, '4文字以上で入力してください。'),
})
const schema = z.object({
name: z.string()
.nonempty('名前を入力してください。')
.min(4, '4文字以上で入力してください。')
})
Select
const schema = yup.object({
area: yup.number()
.transform((value, originalvalue) => originalvalue === '' ? undefined : value)
.required('いずれかを選択してください。')
})
const schema = z.object({
area: z.union([z.string(), z.number()])
.refine(
(val) => val !== '', { message: 'いずれかを選択してください。' }
)
})
RadioGroup
const schema = yup.object({
gender: yup.number().moreThan(-1,'いずれかを選択してください。')
})
zod の場合は RadioGroup の選択値は number で受け取れず string になったので型変換し、その後にチェーンでnumber.positive()
が使えなかったので.refine()
で検証しています。
const schema = z.object({
gender: z.number()
.or(z.string().transform(Number))
.refine(
(val) => val > 0, 'いずれかを選択してください。')
})
DatePicker
DatePickerで未入力の時の値はnullになります。
yupではtransform()
で null を undefined に変換して、required()
で必須チェックしています。
またDatePickerは日付として中途半端な値を入力することができますが、この中途半端な日付の値をtypeError()
でチェックしています。
const schema = yup.object({
applicationDate: yup.date()
.transform((value, originalvalue) => originalvalue == null ? undefined : value)
.required('申請日を入力してください。')
.typeError('日付を正しく入力してください。')
})
zod の場合はz.date()
だけで、必須チェックと日付として中途半端な値のチェックができます。
日付として中途半端な値のときのエラー「invalid_date」のメッセージを変更するのに、エラーマップでメッセージを設定するしか方法がありません。
エラーマップはグローバルで設定したり、下記のように個別に設定することも可能です。
通常は汎用的なメッセージを指定しておけば問題ないですが、個別のメッセージを表示しなければいけない場合は面倒です。
const errMap: z.ZodErrorMap = (issue, _ctx) => {
let message
switch (issue.code) {
case z.ZodIssueCode.invalid_type:
message = `申請日を入力してください。`
break
case z.ZodIssueCode.invalid_date:
message = `日付を正しく入力してください。`
break
default:
message = _ctx.defaultError
}
return { message }
}
const schema = z.object({
applicationDate: z.date({errorMap: errMap})
})
複数のCheckBox
スキーマchecks: yup.array().of(yup.boolean().required()).required()
(型推論boolean[]
)の定義は冗長です。
const schema = yup.object({
checks: yup.array().of(yup.boolean().required()).required(),
checkerr: yup.boolean().required().test('checklength', 'いずれか2つ選択してください。', function() {
return this.parent.checks.filter((v:boolean) => v === true).length >= 2
})
})
const schema = z.object({
checks: z.array(z.boolean()),
checkerr: z.boolean()
})
.refine(data => data.checks.filter((v) => v === true).length >= 2, {
message: 'いずれか2つ選択してください。',
path: ['checkerr'],
})
結論
yup にするか zod にするか?
サンプルコードを書く限り、そんなに大きな違いがあるようには思えませんでした。
個人的な感想ですが、
yup のほうがコード量は増えますが、ライブラリの動作を把握していなくてもパッと見で仕様が理解できるのではと思いました。
また、今回サンプルコードを書いてみるにあたって、yupで詰まった所は検索すれば解決策がすぐヒットするのに対して、zod はあまり情報がヒットせずに少し時間がかかりました。
zod も慣れればそんなに迷わず書けそうです。
しかし yup は初見から迷わずに書けました。
ということで、私は yup にしようと思います。
Discussion