👨‍✈️

React Hook Formとyupで相関チェックを行う(2つ以上の値を見てバリデーションする)

2023/04/04に公開

バージョン

  • react 18.2.0
  • react-hook-form 7.4.3.8
  • yup 1.0.2
  • @hookform/resolvers 3.0.0

はじめに

※この記事ではReact Hook Form と yupの基本的な利用方法は解説しません。

まずは答えとなるコードを記載します。
動作確認はcodesandboxを用意しました。
https://codesandbox.io/s/react-hook-form-v7-yup-v1-b46v15?file=/src/App.tsx

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";

const labelStyle = { display: "block" };

// 相関チェック
const cityDomain = yup
  .string()
  .required()
  .test(
    "check under 19 and C city",
    "Areas not covered by delivery.",
    function (this: yup.TestContext, city: string | undefined | null) {
      const valid = true;
      const inValid = false;
      const { age } = this.parent;
      if (city === undefined) return valid;
      if (city === null) return valid;

      // 二十歳に満たないかつ,c cityに住んでいる場合はエラー
      return age < 20 && city === "c city" ? inValid : valid;
    }
  );

const schema = yup.object({
  firstName: yup.string().required(),
  age: yup.number().positive().integer().required(),
  city: cityDomain
});

type FormData = yup.InferType<typeof schema>;

export default function App() {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm<FormData>({
    resolver: yupResolver(schema)
  });
  const onSubmit = (data: FormData) => console.log(data);

  return (
    <>
      <h1>Pizz Delivady</h1>
      <h2>Please enter your information</h2>
      <form onSubmit={handleSubmit(onSubmit)}>
        <label htmlFor="firstName" style={labelStyle}>
          First Name
        </label>
        <input id="firstName" {...register("firstName")} />
        <p>{errors.firstName?.message}</p>

        <label htmlFor="age" style={labelStyle}>
          Age
        </label>
        <input id="age" {...register("age")} />
        <p>{errors.age?.message}</p>

        <label htmlFor="city" style={labelStyle}>
          City
        </label>
        <select id="city" {...register("city")}>
          <option value="a city">A City</option>
          <option value="b city">B City</option>
          <option value="c city">C City</option>
        </select>
        <p>{errors.city?.message}</p>

        <input type="submit" disabled={!!(errors.firstName || errors.age)} />
      </form>
    </>
  );
}

やりたいこと

要件は以下の通り

  1. ピザのデリバリーアプリ
  2. 名前、年齢、お住いの市名を入力してもらう
  3. 名前は文字列、年齢は数字で入力されている場合のみ送信ボタンを活性
  4. 年齢が19歳以下かつc cityにお住まいの場合は「配達範囲外」である旨のメッセージを市選択プルダウンの下に表示する(本来こんな理不尽?意味不明なエラー理由ではお店は潰れそうですが美味しいピザなので生き残っています)

相関チェックのやり方について

この記事で見ていきたいのは4の項目です。
4の項目を実現しているのは次のコード

const cityDomain = yup
  .string()
  .required()
  .test(
    "check under 19 and C city",
    "Areas not covered by delivery.",
    function (this: yup.TestContext, city: string | undefined | null) {
      const valid = true;
      const inValid = false;
      const { age } = this.parent;
      if (city === undefined) return valid;
      if (city === null) return valid;

      // 二十歳に満たないかつ,c cityに住んでいる場合はエラー
      return age < 20 && city === "c city" ? inValid : valid;
    }
  );

上から順にみていきます。
2つ以上の入力を使ってバリデーションするにはtestを利用します。
testは3つの引数を受け取ります。

  1. 第一引数はtestの内容を説明する文字列を渡します。何を書いてもよさそうです。
  2. 第二引数はバリデーションエラー時に出力するメッセージを渡します。渡したメッセージはerrors.city.messageに入ってきます。
  3. 第三引数は入力の値を引数に取る関数を渡します。この中で値の検証を行っていくので詳しくfunctionの中身を見ていきます。
function (this: yup.TestContext, city: string | undefined | null) {
      const valid = true;
      const inValid = false;
      const { age } = this.parent;
      if (city === undefined) return valid;
      if (city === null) return valid;

      // 二十歳に満たないかつ,c cityに住んでいる場合はエラー
      return age < 20 && city === "c city" ? inValid : valid;
    }

functionは2つの引数を受け取ります。

  1. 第一引数はschemaにアクセスすることができる変数です。parentというプロパティの中にschemaが入っています。
  2. 第二引数はcityが入っています。cityプルダウンでA Cityを選択していれば文字列"A City"が入っています。

あとは第一引数と第二引数を利用して条件式を書いて、エラーの場合はfalseを、問題なければtrueを返します。
falseが返ったときのみerrors.city.messageに値が入ります。

      // 二十歳に満たないかつ,c cityに住んでいる場合はエラー
      return age < 20 && city === "c city" ? inValid : valid;

以上です。

Discussion