Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save pipopotamasu/e279a572b6c843af305b0615289f93f9 to your computer and use it in GitHub Desktop.
Save pipopotamasu/e279a572b6c843af305b0615289f93f9 to your computer and use it in GitHub Desktop.

タイトル

react-hook-formとyupを使った重複チェックバリデーション

初めに

最近yupに値の重複チェックするためのいい感じの機能が入ったので、react-hook-formと組み合わせて重複チェックをする方法を備忘のため書き綴っていきます。 今まではデータのネストが深い場合、ネストを遡って他のデータにアクセスしデータのバリデーションが難しかったのですが、yupのv0.29.1から追加されたtest関数のコンテキストのfromプロパティを使用することによってネストを遡ることが容易になりました。
jquense/yup#556

今回はそのfromを使用し、フォームの重複チェックバリデーションを作ってみます。

以下は今回のサンプルで使用しているライブラリのバージョンです。

react, react-dom: 16.31.1
react-hook-form: 6.0.2
yup: 0.29.1

重複チェックバリデーション

重複チェックしたいシチュエーション

まずいきなり重複チェックをする方法を記載する前に、重複チェックをしたいシチュエーションを考えてみたいと思います。 以下のように、複数の名前とメールアドレスを入力できるフォームをまず考えてみましょう。

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

const schema = yup.object().shape({
  users: yup.array(
    yup.object().shape({
      name: yup.string().required('name is required.'),
      email: yup
        .string()
        .required('email is required.')
        .email('invalid email type.')
    }),
  ),
});

function Form() {
  const { control, register, errors, handleSubmit } = useForm({
    mode: 'onBlur',
    defaultValues: { users: [{ name: '', email: '' }] },
    resolver: yupResolver(schema),
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'users',
  })

  const userErrors = errors.users;

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>
      <ul>
        {
          fields.map((user, i) => (
            <li key={user.id}>
              <input
                name={`users[${i}].name`}
                type="text"
                defaultValue={user.name}
                ref={register}
              />
              <input
                name={`users[${i}].email`}
                type="text"
                defaultValue={user.email}
                ref={register}
              />
              <button
                type="button"
                onClick={() => remove(i)}
              >
                remove
              </button>
              <p style={{ color: 'red' }}>
                <span>
                  { userErrors && userErrors[i]?.name?.message }
                </span>
                <span>
                  { userErrors && userErrors[i]?.email?.message }
                </span>
              </p>
            </li>
          ))
        }
      </ul>
      <button type="button" onClick={() => append({ name: '', email: '' })}>add</button>
      <button type="submit">submit</button>
    </form>
  );
}

UI的には以下のようになります(※CSSまともに入れてないので見にくいです)。

sample1

このように、ユーザーが名前とメールアドレスを追加/削除し最終的には登録できるようなUIです。

現時点のサンプルコードには必須入力チェックとメールアドレスのフォーマットチェックしか入ってません。 さて、ここでアプリケーションの要件として「メールアドレスの重複を弾く」というものがある場合、Form側にもユーザの入力に対して重複チェックを入れたいです。 既存のバリデーションに加え、重複チェックも追加していきましょう。

重複チェックバリデーションの追加

yupをベースに重複チェックバリデーションを追加しますので、yupのemailに対して新しいバリデーションの定義を追加しましょう。

const schema = yup.object().shape({
  users: yup.array(
    yup.object().shape({
      name: yup.string().required('name is required.'),
      email: yup
        .string()
        .required('email is required.')
        .email('invalid email type.')
        .test('email-dup', 'duplicated email', () => { /* バリデーションロジック */ }) // 新しく追加
    }),
  ),
});

上記のように重複バリデーション用の定義を追加しました。 まだバリデーションロジックは書いておらず空の無名関数を渡しているだけですが、今からここに重複チェックのロジックを追加していきます。

function validateDuplicatedEmail(email) {
  const { users } = this.from[1].value;
  if (users.length < 2) return true;

  let dupCount = 0;
  for (let i = 0; i < users.length; i += 1) {
    if (users[i].email === email) {
      dupCount += 1;
      if (dupCount > 1) {
        return false;
      }
    }
  }

  return true;
}

const schema = yup.object().shape({
  users: yup.array(
    yup.object().shape({
      name: yup.string().required('name is required.'),
      email: yup
        .string()
        .required('email is required.')
        .email('invalid email type.')
        .test('email-dup', 'duplicated email', validateDuplicatedEmail)
    }),
  ),
});

validateDuplicatednEmailというバリデーション用の関数を定義しました。 その名の通り、重複するメールアドレス用のバリデーション関数です。 一応これで重複チェックバリデーション自体は完成なのですが、冒頭で言及したfromについて詳しくみていきましょう。

this.fromについて

冒頭でも説明したようにこれはyupのv0.29.1から入った機能です。 この機能の説明をする前に、なぜこの機能が追加されたかと合わせて話しておきたいと思います。

上記のコードでも使用していますが、yupには独自のバリデーションを定義できるtestという関数が用意されています。 そのtest関数には特別なコンテキストが渡されるようになっており、その中にthis.parentという親オブジェクトを参照するためのプロパティが用意されています。

例えば今回test関数はemailというプロパティに対して設定されています。 その親のオブジェクト{ name: '', email: '' }にアクセスするためのものがthis.parentになります。

さて、今回のようにメールアドレスの重複チェックをする場合、さらにネストを遡って配列内の他のオブジェクトにアクセスする必要があります。

[{ name: 'name1', email: 'foo@bar.com' },  { name: 'name2', email: 'foo@bar.com' }]

emailの親のオブジェクトよりさらに一段上がってここまでアクセスできないと重複チェックができないわけです。

しかし、v0.29.1より前のバージョンでは親の親にアクセスする手段が提供されていませんでした。

そこで追加されのがthis.fromです。 これによりネストをどこまでも遡れるようになりました。

例えば上記サンプル内の以下のコード。

const { users } = this.from[1].value;

this.from[0]で親のオブジェクト{ name: '', email: '' }this.from[1]でさらに親のオブジェクト{ users: [{ name: '', email: '' }] }まで遡れます。

本来仕様的にthis.from[1]でアクセスできるのは[{ name: '', email: '' }]の配列だと思うのですが、なぜかその親のオブジェクトへのアクセスとなっております。この点については仕様確認のissueを投げています。

これにより格段に重複チェックが楽になりました。

重複バリデーションを組み込んだUIは以下のようになります。

sample2

終わりに

今回はyupのv0.29.1で追加されたfromとreact-hook-formを組み合わせた重複チェックのバリデーションを紹介させていただきました。

なおこのバリデーションで1つ注意点があります。 サンプルコードの実装ではinputのblurイベントでバリデーションが動作するようになっています。 そしてバリデーションはそれぞれの行に対して走るので、validateDuplicatedEmail内のloop処理が合わさると計算量がO(n^2)になります。 少ない行数なら問題になりませんが、行数が多くなるにつれ計算量が指数関数的に増えるので注意してください。

本文ではサンプルコードを断片的に記載したので、以下に今回のサンプルコードの全体像を記載します。

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

function validateDuplicatedEmail(email) {
  const { users } = this.from[1].value;
  if (users.length < 2) return true;

  let dupCount = 0;
  for (let i = 0; i < users.length; i += 1) {
    if (users[i].email === email) {
      dupCount += 1;
      if (dupCount > 1) {
        return false;
      }
    }
  }

  return true;
}

const schema = yup.object().shape({
  users: yup.array(
    yup.object().shape({
      name: yup.string().required('name is required.'),
      email: yup
        .string()
        .required('email is required.')
        .email('invalid email type.')
        .test('email-dup', 'duplicated email', validateDuplicatedEmail)
    }),
  ),
});

function Form() {
  const { control, register, errors, handleSubmit } = useForm({
    mode: 'onBlur',
    defaultValues: { users: [{ name: '', email: '' }] },
    resolver: yupResolver(schema),
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'users',
  })

  const userErrors = errors.users;

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>
      <ul>
        {
          fields.map((user, i) => (
            <li key={user.id}>
              <input
                name={`users[${i}].name`}
                type="text"
                defaultValue={user.name}
                ref={register}
              />
              <input
                name={`users[${i}].email`}
                type="text"
                defaultValue={user.email}
                ref={register}
              />
              <button
                type="button"
                onClick={() => remove(i)}
              >
                remove
              </button>
              <p style={{ color: 'red' }}>
                <span>
                  { userErrors && userErrors[i]?.name?.message }
                </span>
                <span>
                  { userErrors && userErrors[i]?.email?.message }
                </span>
              </p>
            </li>
          ))
        }
      </ul>
      <button type="button" onClick={() => append({ name: '', email: '' });}>add</button>
      <button type="submit">submit</button>
    </form>
  );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment