Open10

react-hook-form + yup tips

ピン留めされたアイテム
high-ghigh-g

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 の記述でも型エラーにならない。

high-ghigh-g

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
)

参考
https://tech.motoki-watanabe.net/entry/2021/01/23/205510

date型のチェック

date()を利用する
ただ、nullが発生したり、実際のフォームはstring型で渡ってきたりする為、
date()の扱い方は以下の様に少し工夫が必要

yup
  .date()
  .nullable()
  .typeError('正しい日付を入力しましょう')
  .required('必須エラーメッセージ')
high-ghigh-g

フォーム値による分岐

別のフォーム値によって、バリデーションを発生させるかどうかのif的な切り替えをしたい場合、
when を利用する

yup.object().shape({
    showEmail: yup.boolean(),
    email: yup
      .string()
      .email()
      .when("showEmail", {
        is: true,
        then: yup.string().required("Must enter email address")
      })
  })
high-ghigh-g

カスタムバリデーション

.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側のバリデーションで利用できる様になる。

high-ghigh-g

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[]を表現できる

high-ghigh-g

.test()がうまく動作しない問題
https://github.com/jaredpalmer/formik/issues/2146

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,
      })
    }),