【React】React Hook FormとYup を使ったフォームバリデーションのススメ
フロントエンドエンジニアのささもりです!
HTMLコーダー歴5年、React(Next.js)歴半年の僕が、実務でフォームバリデーションを実装することになりました。
当時「フォームバリデーションってなんだ?」「なんで自分で組まずライブラリを使うんだ?」「何でスキーマビルダーを使うんだ?」状態だったので、備忘録として学んだことをメモします。
間違っていたらコメントでご指摘いただけると幸いです!
対象読者
- React(Next.js)でフォームバリデーションを実装したい方
- フォームバリデーションライブラリのメリットがわからない方
- バリデーションライブラリとスキーマビルダーってなんのことだ?状態の方
使った技術
- React(Next.js)
- TypeScrpt
- React Hook Form
- Yup
フォームバリデーションとは
フォームバリデーションとは、ユーザーがフォームに値を入力した時に、適切な値かをフロント側でチェックしてユーザーにフィードバックする機能です。
バリデーションライブラリとは
フォームバリデーションを簡単に実装するためのライブラリです。
今回は React Hook Form を採用しました。
バリデーションライブラリ(React Hook Form)を使った実装例
バリデーションライブラリを使ったフォームバリデーションの実装例です。
import { useForm, SubmitHandler } from "react-hook-form";
// フォーム項目の型を定義
type Forms = {
inputBox: string;
inputBoxReuired: string;
};
function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<Forms>();
const onSubmit: SubmitHandler<Forms> = () => {
// バリデーション通過後の処理
alert("送信しました!");
};
return (
<div className="App">
<form onSubmit={handleSubmit(onSubmit)}>
{/* 入力任意のinput */}
<div>
<input placeholder="任意項目" {...register("inputBox")} />
</div>
{/* 入力必須のinput */}
<div>
<input
placeholder="必須項目"
{...register("inputBoxReuired", { required: true })}
/>
<div>{errors.inputBoxReuired && "必須項目です。"}</div>
</div>
{/* 送信ボタン */}
<input type="submit" />
</form>
</div>
);
}
export default App;
注目すべきは以下の5項目です。
-
type Forms
にフォームの項目の型を定義している -
useForm
を使って、React Hook Formの関数を呼び出している - 項目に対するバリデーションの設定は 2のregister関数を使って
{...register("inputBox")}
のように定義する - バリデーション条件を付け加えるときは
{...register("inputBoxReuired", { required: true })}
のようにregisterの後ろに追加する - エラーの状態は2のerrorsオブジェクトの中に格納される
register関数を使ってバリデーションを自由に追加でき、わかりやすい構成になっています。
その上でバリデーションライブラリを使うと以下のようなメリットがあると言えます。
バリデーションライブラリを使うメリット
今回バリデーションライブラリを導入した理由は、以下のようなメリットがあると感じたためです。
- 自分で実装するよりも手軽
- ライブラリに備わっている豊富なバリデーションを使える
- ライブラリに則った実装により、ページの実装のばらつきを減らす
自分で実装するよりも手軽
フォームバリデーションはライブラリを使わず実装できます。
簡単なフォームバリデーションであれば、自分でコードを書いても負担は少ないかもしれません。
しかしプロダクトの規模が大きくなると、多種多様なバリデーションが欲しくなることが考えられます。
必須チェック、型(文字、数値、日付…)チェック、日付の前後チェック…
さらにフォームデータ・エラーメッセージなど、複数のStateを用いてデータを管理する必要があるため、実装が複雑になります。
ライブラリを用いることで、ライブラリの関数を使ってシンプルにバリデーションを実装できます。
ライブラリに備わっている豊富なバリデーションを使える
ライブラリには必須チェックや型チェック、カスタマイズでバリデーションを作るための機能などがすでに備わっています。
1つ目の理由と重複しますが、自分で実装するよりもライブラリに備わっている機能を使って、サクサク実装したいという狙いです。
ライブラリに則った実装により、実装ルールを共通化できる
react-fook-formにはフォームの値を取得したり、セットしたりできる独自の関数が備わっています。
これらを自前で組もうとすると、実装者によってコードのばらつきが生じ、プロダクトの規模が大きくなったときにコード管理が大変になることが考えられます。
ライブラリのルールに則って実装することで、エンジニア間による実装のバラつきを最小限に抑えられると考えました。
それでは次に、スキーマビルダーを使った実装を見てみましょう。
スキーマビルダーとは
バリデーションをスキーマで定義するためのパッケージです。
スキーマビルダーはReact Hook Formと組み合わせて使います。
🙋「スキーマビルダーって何のためにあるの?」
🙋「React Hook Formがあればフォームバリデーションを実装できるのに、なんでスキーマビルダーを使うの?」
スキーマビルダーについて、このような疑問が湧く方もいるのでは無いでしょうか。
スキーマビルダーを用いることで、バリデーションルールをスキーマ形式で定義できるようになります。なんのこっちゃ。
react-hook-formがサポートしているスキーマビルダーが公式ドキュメントに記載されているので、その中からどれかを選べば間違い無いかと思います。
- Yup
- Zod
- Superstruct
- Joi
今回は Yup を採用したので、
React Hook Formのみの実装と、React Hook Form + Yupの実装を比べてみましょう。
スキマービルダー(Yup)を使うor使わないで実装を比較
React Hook Formのみ
type Forms = {
...
inputBoxReuired: string;
};
....
<input
placeholder="必須項目"
{...register("inputBoxReuired", { required: true })}
/>
先ほどのReact Hook Formの実装から、input要素(必須項目)のコードを抜粋しました。
requiredのみであれば可読性に問題を感じることは少ないかもしれません。
では今度は「必須項目」「10文字まで」「カタカナのみ」の、katanakaRequiredという項目を設定してみます。
katanakaRequiredにはそれぞれのエラーパターンに応じたメッセージが定義されています。
type Forms = {
...
katanakaRequired: string;
};
....
<input
placeholder="カタカナ必須項目"
{...register("katanakaRequired", {
required: "必須項目です",
maxLength: {
value: 10,
message: "10文字以内にしてください",
},
pattern: {
value: /^[ァ-ヶー ]*$/,
message: "カタカナで入力してください",
},
})}
/>
複雑なコードになりました。このコードが至る所に定義されていると、メンテナンスが大変なのは明らかです。
katanakaRequiredに定義するルールを変えるときは、全ての箇所を修正する必要があります。
React Hook Form + Yup
ではスキーマビルダーのYupを導入したコードを見てみましょう。
- inputBox: 任意の文字列要素
- katanakaRequired: 「必須項目」「10文字まで」「カタカナのみ」の文字列要素
この2つの項目を定義してみます。
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
export default function Home() {
const schema = yup.object({
inputBox: yup.string(),
katakanaRequired: yup
.string()
.max(10, "10文字以内で入力してください。")
.test("katakana", "カタカナで入力してください。", function (value: any) {
return !!value.match(/^[ァ-ヶー ]*$/);
})
.required("必須項目です。"),
});
type FormValues = yup.InferType<typeof schema>;
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(schema),
});
const onSubmit = (data: FormValues) => {
// バリデーション通過後の処理
alert("送信しました!");
};
return (
<div className="App">
<form onSubmit={handleSubmit(onSubmit)}>
{/* 入力任意のinput */}
<div>
<input placeholder="任意項目" {...register("inputBox")} />
</div>
{/* 入力必須のinput */}
<div>
<input placeholder="必須項目" {...register("katakanaRequired")} />
<div>
{/* katakanaRequired エラーメッセージの出力 */}
{errors.katakanaRequired && errors.katakanaRequired.message}
</div>
</div>
{/* 送信ボタン */}
<button type="submit">送信</button>
</form>
</div>
);
}
以下の点に注目してください。
-
const schema
の1箇所でバリデーションルールを定義している - inputには
register
と項目名のみ設定している -
type FormValues = yup.InferType<typeof schema>;
でフォームの型を生成している
その上で以下のようなメリットがあるといえます。
バリデーションスキーマを使うメリット
バリデーションルールを一箇所で定義し、可読性が向上する
バリデーションルールを一箇所で定義し、register関数には項目の名前だけを指定すれば良くなりました。
ルールを変更するときは、スキーマで定義されているルールを変更するのみでOKです。
シンプルで読みやすいですね。
スキーマ定義からフォームの型を生成できる
type FormValues = yup.InferType<typeof schema>;
この型を覗くと以下のように定義されています。
文字列であることと、任意か必須かを型推論してくれています。
このtypeをごちゃごちゃして色々なところで使いまわせるので便利です。
Yupに用意されている豊富なスキーマを使える
Yupはバリデーションスキーマに特化しているので、React Hook Formよりも多種多様なバリデーションが用意されています。
React Hook Formだと痒いところに手が届かない、となったときにYupが解決してくれるかもしれません。
またYupのカスタムバリデーションを作るtest()を使えば、独自のバリデーションも実装できます。(カタカナバリデーションの実装がその例です)
React Hook FormとYupを組み合わせて使ってみよう
よきフォームバリデライフを!
Discussion