react-hook-form + yup tips

react-hook-form側の基本知識
今回、yupのtips紹介がメインの為、詳細なreact-hook-formの解説は省きます。
動作イメージ
利用の為の基本記述
※一旦、型なしで書きます
import { yupResolver } from '@hookform/resolvers/yup';
import { useForm } from 'react-hook-form';
const errorScheme = yup.object().shape({
// ここがreact-hook-form側と一致しているとバリデーションが発火
form_name: yup
.string()
.required('エラーメッセージ'),
});
const component = () => {
const {
handleSubmit,
formState: { errors },
} = useForm({
mode: 'all',
criteriaMode: 'all',
shouldFocusError: false,
defaultValues: {
form_name: '初期値', // ここがyup側と一致しているとバリデーションが発火
},
resolver: yupResolver(errorScheme),
});
// ... 省略
}
useFromに渡すパラメータについて
mode
バリデーションの実行タイミング
onChange, onBlur, onSubmit, onTouched のタイミングが指定でき、
allだとすべてのタイミングでバリデーション実行
criteriaMode
バリデーション時に発生したエラーの表示モード
firstError → エラー一つだけ表示
all →すべてのエラーを表示
shouldFocusError
フォームが送信されて、エラーが含まれているときにエラーのある最初のフィールドにフォーカスする
defaultValues
初期値設定
※これが設定できてないとreact-hook-form, yup共に動きません
resolver
yupを動かすための設定
型で詰まったポイント
useFormに対しての型
defaultValuesに記述した型の通りに記述
errors周り
formState: { errors }, で取得するerrorsに対して型付けを行う場合、
string, numberなどのプリミティブなエラーに対しては、FieldError という型で良いが、
string[], number[]などの配列型のエラーに関しては、FieldError[]ではなく、FieldErrorsを利用した方が都合が良い。
FieldError[]としてしまうと、エラー記述をマークアップする際に、messageのプロパティが取得しにくいが、FieldErrorsだとerrors.formName?.message の記述でも型エラーにならない。

yup Tips
文字列 + 必須チェックの場合
yup
.string()
.required('必須エラーメッセージ')
数値 + 必須チェックの場合
yup
.number()
.required('必須エラーメッセージ')
配列型の必須チェックの場合
arrayには.requiredが利用できなかった為、.min(1 , ◯◯)を指定することで、1つ以上の選択がない場合にエラーを表示するという意味で実装する
yup
.array()
.min(1, '必須エラーメッセージ')
numberだけどnullも許す感じでバリデーションしたいとき
フォームから取得した値をtransform内で一旦string化し、trimした後、
空文字ならnullを返し、そうでなければ数値を返すといった処理
yup
.number()
.nullable()
.transform((value, originalValue) =>
String(originalValue).trim() === '' ? null : value
)
参考
date型のチェック
date()を利用する
ただ、nullが発生したり、実際のフォームはstring型で渡ってきたりする為、
date()の扱い方は以下の様に少し工夫が必要
yup
.date()
.nullable()
.typeError('正しい日付を入力しましょう')
.required('必須エラーメッセージ')

フォーム値による分岐
別のフォーム値によって、バリデーションを発生させるかどうかのif的な切り替えをしたい場合、
when
を利用する
yup.object().shape({
showEmail: yup.boolean(),
email: yup
.string()
.email()
.when("showEmail", {
is: true,
then: yup.string().required("Must enter email address")
})
})

カスタムバリデーション
.testを利用する
.test内で他フォームの値を参照したい場合、this.parentを利用する
※ちなみにfunctionを利用しているのは、アロー関数にするとthisの参照が狂う為
.test(function(value) {
// ここにthis.parent.◯◯で記述
this.parent.formName
})
サンプルコード
formCalendar: yup
.date()
.nullable()
.typeError(ERROR_MESSAGE.REQUIRED_CALENDAR_SELECTED)
.required(ERROR_MESSAGE.REQUIRED_CALENDAR_SELECTED)
.test('', function (value) {
const deadline = this.parent.hiddenFrom
const selectedDay = dayjs(value)
const deadlineDay = dayjs().add(+deadline, 'day')
return dayjs(selectedDay).isAfter(deadlineDay);
}),
hiddenFrom: yup.string(),
もし、yup内でReactのステートやRecoilのグローバルステートの値を利用したい場合、
ステートの値はyup側に直接記述できない為、
仮想的なフォームをreact-hook-form上に生成し、その仮想的なフォームに対して、
ステートの値をsetValue()でセットすれば、yup側でthis.parent.◯◯で値を参照することができる。
こうすることで常に動的に変わるステートもyup側のバリデーションで利用できる様になる。

yupでstring[]にするスキーマの書き方
keywords: array()
.of(string().required())
.required()
.min(1, ERROR_MESSAGE.REQUIRED_TEXT)
.max(10, ERROR_MESSAGE.MAX_ADD_LENGTH),
array()だけだとany[]になる為、array().of(string())を書かないといけないが、
これだと(string | undefined)[]になる為、array().of(string().required())とする。
さらに、string[] | undefinedとならないように、requiredを繋げて、
array().of(string().required()).required()とすると、string[]を表現できる

.test()がうまく動作しない問題
pathを設定するのが重要らしい
return this.createError({
path: "field1 | field2",
message: "One field must be set",
})
複数の条件下で.test()を利用する場合、以下でクリアできた。
keywords: array()
.required(ERROR_MESSAGE.REQUIRED_TEXT_ARRAY)
.min(1, ERROR_MESSAGE.REQUIRED_TEXT_ARRAY)
.of(
object().shape({
keyword: string()
.test('BLACKLIST', ERROR_MESSAGE.BLACKLIST, (text, ctx) => {
if (BLACKLIST_REGEX.test('' + text)) {
return ctx.createError({
message: ERROR_MESSAGE.BLACKLIST,
path: ctx.path,
})
}
return true
})
.test('MAX_TEXT_LENGTH', KEYWORD_LENGTH + ERROR_MESSAGE.MAX_LENGTH, (text, ctx) => {
if (('' + text).length > KEYWORD_LENGTH) {
return ctx.createError({
message: KEYWORD_LENGTH + ERROR_MESSAGE.MAX_LENGTH,
path: ctx.path,
})
}
return true
}),
})
)
.test('MIN_REQUIRED', ERROR_MESSAGE.REQUIRED_TEXT_ARRAY, (list, ctx) => {
const res = list.some((item) => item.keyword && item.keyword.length > 0)
if (res) {
return true
}
return ctx.createError({
message: ERROR_MESSAGE.REQUIRED_TEXT_ARRAY,
path: ctx.path,
})
}),