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まともに入れてないので見にくいです)。
このように、ユーザーが名前とメールアドレスを追加/削除し最終的には登録できるような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
について詳しくみていきましょう。
冒頭でも説明したようにこれはyupのv0.29.1から入った機能です。 この機能の説明をする前に、なぜこの機能が追加されたかと合わせて話しておきたいと思います。
上記のコードでも使用していますが、yupには独自のバリデーションを定義できるtest
という関数が用意されています。
そのtest
関数には特別なコンテキストが渡されるようになっており、その中にthis.parent
という親オブジェクトを参照するためのプロパティが用意されています。
例えば今回test
関数はemailというプロパティに対して設定されています。
その親のオブジェクト{ name: '', email: '' }
にアクセスするためのものがthis.parent
になります。
さて、今回のようにメールアドレスの重複チェックをする場合、さらにネストを遡って配列内の他のオブジェクトにアクセスする必要があります。
[{ name: 'name1', email: '[email protected]' }, { name: 'name2', email: '[email protected]' }]
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は以下のようになります。
今回は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>
);
}