🪐

Yup フォーム・バリデーションの実装におけるデバッグ Tips

2024/05/10に公開

Web アプリケーションでフォームの入力値バリデーションを実装するとき、Yup は強力で柔軟性が高いツールの一つです。
Yup を利用すると、スキーマベースのアプローチで入力値のバリデーションを簡潔かつ効率的に行うことができます。

https://github.com/jquense/yup

しかし、複雑なフォームの実装になるとスキーマ定義の実装時、特に TypeScript を併用している環境では、型エラーや実装の難点に直面することがあります。

今回の記事では、具体的なフォームの実装例を用いながら「複雑なスキーマ定義の実装」で直面する類似の問題をよりスムーズに解決できる Tips を紹介していきます。

紹介する Tips

これらが本記事で紹介する Tips です。
フォームの実装には React Hook Form の利用を想定しています。

想定シナリオとフォーム実装

Tips を紹介する前に、想定しているシナリオと仕様およびフォーム実装のイメージを示します。
今回は「複雑なスキーマ定義」の具体例として、ユーザーの選択肢によって入力項目とバリデーション・ルールが変化するフォーム仕様を題材にします。

想定シナリオ


架空のイベント「未来科学フェア」

想定しているシナリオは架空のイベント「未来科学フェア」です。
あなたはこのイベントの登録フォームを作成する任務にあたっています。参加者がイベントに登録するには、まず「時間旅行者」と「エイリアン」という二つのカテゴリーからひとつを選択する必要があります。この選択に基づいて、参加者が提出する登録内容のバリデーション・ルールが変化します。


登録フォームのイメージ:左は時間旅行者、右はエイリアンを選択した場合

  • 時間旅行者として参加する場合:
    • 参加者は「何年から来ましたか?」という質問に答える必要があります。
    • この入力は数値であり、さらに 2025 年以降の未来の年である必要があります。
    • この条件を確認するために、Yup の数値バリデーション機能を使って、入力が数値であり、かつ指定された年数を超えていることを確認するスキーマを定義します。
  • エイリアンとして参加する場合:
    • こちらの参加者は「あなたの星の名前は?」という質問に答えます。
    • この回答は文字列であり、特定のキーワード(例えば、「星」や「星座」)を含む必要があります。
    • この条件を確認するために、Yup の文字列バリデーションを用いて、特定の文字列が含まれているかを検証するルールを設定します。

フォームの実装

こちらがフォームの実装イメージです。(わかりやすくするため、一部のコードを省略しています。)

ふたつの異なるバリデーション要件をうまく管理するために、Yup の when 構文を用いて条件に応じた動的なスキーマ変更を行ってます。これによって、フォームがどのように応答するかを柔軟に設計することができます。

このコード例を用いて、以降のセクションで Tips を解説します。

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

const myFormSchema = yup.object().shape({
    name: yup.string().required('お名前を入力してください。'),
    participantType: yup.string().oneOf(["timeTraveler", "alien"]).required(),
    year: yup.number().when("participantType", {
        is: (val: string) => val === "timeTraveler",
        then: (schema) => schema.required().min(2025, "2025 年以降を入力してください。"),
        otherwise: (schema) => schema.notRequired(),
    }),
    planetName: yup.string().when("participantType", {
        is: (val: string) => val === "alien",
        then: (schema) => schema.required().matches(/|/, "星か星座の名前を入力してください。"),
        otherwise: (schema) => schema.notRequired(),
    }),
});
type MyFormType = yup.InferType<typeof myFormSchema>;

const useMyForm = () => {
    return useForm<MyFormType>(
        {
            resolver: yupResolver(myFormSchema)
        }
    );
}
MyForm.tsx
import type {MyFormType} from "@/app/MyForm/use-my-form";
import {useMyForm} from "@/app/MyForm/use-my-form";

export const MyForm = () => {
    const { register, handleSubmit, formState: {errors}, watch } = useMyForm();

    const participantType = watch("participantType");

    const onSubmit = (data: MyFormType) => {
        console.log(data)
    }

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <label>お名前は?</label>
            <input {...register("name")} />
            {errors.name && <span>{errors.name.message}</span>}

            <label>参加者タイプ</label>
            <input type="radio" {...register("participantType")} id="participantType_timeTraveler" value="timeTraveler"/>
            <label htmlFor="participantType_timeTraveler">時間旅行者 — Time Traveler</label>
            <input type="radio" {...register("participantType")} id="participantType_alien" value="alien"/>
            <label htmlFor="participantType_alien">エイリアン — Alien</label>

            {participantType === "timeTraveler" && (
                <label>西暦何年から来ましたか?</label>
                <input {...register("year")} />
                {errors.year && <span>{errors.year.message}</span>}
            )}

            {participantType === "alien" && (
                <label>出身星の名前は?</label>
                <input {...register("planetName")} />
                {errors.planetName && <span>{errors.planetName.message}</span>}
            )}

            <button type="submit">登録</button>
        </form>
    )
}

Tips

ここからは、開発過程で直面する可能性のある一般的な型エラーや実装の難点に対処するための実用的な Tips を紹介します。これらの Tips は、Yup スキーマの正確な実装を支援し、検証のプロセスをスムーズにすることを目的としています。
実装時のエラーを効率的に特定し対処する方法を学ぶことは、開発をスムーズにし、デバッグ時間の短縮につながります。

  • 型エラーの識別と対処方法
    • handleSubmit() のエラーハンドラーの活用
      • フォームの送信時に handleSubmit() を利用している場合、エラーハンドラーを適切に設定することでコンソール上に発生している型エラーを表示できます。こうすることで何が問題なのかをすぐに把握し、適切な修正が可能になります。
    • デバッグ時の表示設定
      • ラジオボタンの選択によって隠れる入力フィールドがある場合、デバッグ中はすべての入力フィールドを表示するように一時的に変更します。これにより、エラーメッセージが隠れずにすべてのフィールドを確認でき、バリデーションの問題点を明確に特定できます。
  • スキーマの効果的な組み合わせ
    • スキーマのモジュール化
      • 大きなフォームに対しては、スキーマを小さな単位に分割し、それぞれのスキーマを個別にテストした後に組み合わせる方法が有用です。これにより、エラーの特定が容易になり、保守性と再利用性が向上します。
  • 具体例を使った検証
    • isValidSync() の活用
      • 実際の入力値を使ってスキーマが正しく機能しているかどうかを確認するために、isValidSync() 関数を使用します。これは同期的にバリデーションを実行し、即座に結果を得ることができるため、迅速なフィードバックが可能となります。

これらの Tips を実践することで、Yup を用いたフォームバリデーションの効率と正確性を大幅に向上させることができます。次のセクションでは、これらの Tips を具体的なコード例と共にさらに詳しく解説していきます。

1. 型エラーの識別と対処方法

型エラーは開発中によく遭遇する問題で、特に Yup と TypeScript を組み合わせて使用する場合、適切な型アノテーションとバリデーション・ロジックが必要です。
ここでは、具体的なシナリオである「未来科学フェア」の登録フォームを例に、型エラーを識別し対処するのに役立つふたつの Tips を紹介します。

handleSubmit のエラーハンドラーの活用

React Hook Form の handleSubmit() を利用したエラーハンドリングの例です。

React Hook Form では、handleSubmit() の第二引数としてエラーハンドラーを提供することができます。このエラーハンドラーは、フォーム送信時にバリデーションエラーが発生した場合に呼び出され、エラーオブジェクトを受け取ることができます。

以下は、エラーオブジェクトをコンソールに出力する簡単な例です:

MyForm.tsx
import {FieldErrors} from "react-hook-form";

export const MyForm = () => {
    ...

    const onError = (errors: FieldErrors<MyFormType>) => {
        console.log(errors)
    }

    return (
        <form onSubmit={handleSubmit(onSubmit, onError)}>
            ...
        </form>
    );
};

このコード例では、handleSubmit() の第一引数に onSubmit 関数を、第二引数に onError 関数を渡しています。フォームのバリデーション時にエラーがあれば、onError 関数がエラー情報をコンソールに出力し、すぐに問題を把握することができます。
この方法により、実際の入力値がスキーマ定義に合致しているかを即座に確認でき、型エラーの解消につながります。

デバッグ時の表示設定

フォームのデバッグを効率的に行うためには、エラーが発生した際にその内容を明確に把握することが重要です。特に、ラジオボタンなどによる選択肢でフォームの入力フィールドを動的に表示・非表示させている場合、デバッグが困難になることがあります。デバッグ時には、すべてのフィールドを一時的に表示させることで、どのフィールドがエラーを引き起こしているのかを容易に特定できます。

デバッグ時のフィールド表示設定の実装例:

MyForm.tsx
export const MyForm = () => {
    ...

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            ...

            {(participantType === "timeTraveler" || debugMode) && (
                <label>西暦何年から来ましたか?</label>
                <input {...register("year")} />
                {errors.year && <span>{errors.year.message}</span>}
            )}

            {(participantType === "alien" || debugMode) && (
                <label>出身星の名前は?</label>
                <input {...register("planetName")} />
                {errors.planetName && <span>{errors.planetName.message}</span>}
            )}

            ...
        </form>
    )
}

このコードスニペットでは、デバッグモードのトグルを導入しています。デバッグモードが有効な場合、参加者タイプに関係なく全てのフィールドを表示します。これにより、どのフィールドがエラーを引き起こしているかを素早く把握し、問題の解決に移ることができます。

2. スキーマの効果的な組み合わせ

Yup を使用して複雑なフォームのバリデーション・スキーマを管理する際、スキーマをより小さな、管理しやすい単位に分割し、それらを組み合わせるアプローチは有効です。これにより、各部分が独立してテストや再利用が容易になり、全体としての保守性と可読性が向上します。
基本的な個別のスキーマを定義し、これらを一つのメインスキーマに統合します。

スキーマのモジュール化と組み合わせの例:

my-form-type.ts
const nameSchema = yup.string().required('お名前を入力してください。')
const participantTypeSchema = yup.string().oneOf(["timeTraveler", "alien"]).required()
const yearSchema = yup.number().required().min(2025, "2025 年以降を入力してください。")
const planetNameSchema = yup.string().required().matches(/|/, "星か星座の名前を入力してください。")

const myFormSchema = yup.object().shape({
    name: nameSchema,
    participantType: participantTypeSchema,
    year: yup.number().when("participantType", {
        is: (val: string) => val === "timeTraveler",
        then: () => yearSchema,
        otherwise: (schema) => schema.notRequired(),
    }),
    planetName: yup.string().when("participantType", {
        is: (val: string) => val === "alien",
        then: () => planetNameSchema,
        otherwise: (schema) => schema.notRequired(),
    }),
})

この方法では、when 構文を用いて参加者のタイプに応じて適切なスキーマを動的に適用しています。この動的なスキーマ適用は、フォームが複数のユーザーシナリオに対応する際に特に役立ちます。さらに、各スキーマを個別にテストすることで、エラーの特定と修正が格段に容易になり、全体の開発効率が向上します。

3. 具体例を使った検証

実際のフォーム入力値を用いたテスト・プロセスは、最終的な確認手段として非常に重要です。Yup のisValidSync()メソッドを活用することで、スキーマが正しく機能しているかどうかを即座にテストできます。このメソッドは同期的にバリデーションを行い、入力値がスキーマに適合するかどうかの真偽値を返します。

具体例を使ったバリデーションの例:

use-my-form.test.ts
import type {MyFormType} from "@/app/MyForm/use-my-form";
import {myFormSchema} from '@/app/MyForm/use-my-form';

describe('use-my-form', () => {
  const validateFormData = (formData: MyFormType): boolean => {
    const isValid = myFormSchema.isValidSync(formData);
    console.log('isValid:', isValid);
    return isValid;
  };

  test('validate: timeTraveler', () => {
    // テストデータの例
    const testData: MyFormType = {
      name: 'test',
      participantType: 'timeTraveler',
      year: 2025,
    };

    // データの検証を行う
    expect(validateFormData(testData)).toBeTruthy();
  });

  test('validate: alien', () => {
    // テストデータの例
    const testData: MyFormType = {
      name: 'test',
      participantType: 'alien',
      planetName: '木星',
    };

    // データの検証を行う
    expect(validateFormData(testData)).toBeTruthy();
  });
});

このコード・スニペットでは、登録フォームのスキーマを用いて、時間旅行者とエイリアンの入力データを検証しています。各入力データがスキーマに適合するかどうかを確認できます。

この手法は、開発過程でスキーマの正確性を確保するだけでなく、最終的なアプリケーションの品質向上にも貢献します。実際のデータに基づくテストは、理論上の仕様と実際の運用の間のギャップを埋めるのに役立ちます。

結び

いかがでしたでしょうか。この記事を通じて、Yup を用いたフォーム・バリデーションの強力な機能とその実装方法について理解を深めていただけたことと思います。Yup を活用することで、TypeScript 環境下でのフォーム・バリデーションが、単なるエラーチェックを超えた、効率的で柔軟なデータ整合性の確保手段に変わります。

本記事で紹介した Tips は、Yup のポテンシャルを最大限に引き出し、複雑なバリデーション要件にも対応できるようにするためのものです。
開発中に遭遇する問題に対しては、視覚的なフィードバックや適切なエラーハンドリングを通じて、より迅速に対処することが可能です。さらに、スキーマを小さく分割し、組み合わせるアプローチは、複雑性を管理しやすくするだけでなく、スキーマの再利用性を高め、保守が容易なコードへと導きます。

Yup と他のツールを組み合わせたフォーム・バリデーションのアプローチは、開発者がより安全で堅牢なアプリケーションを構築する手助けをするものです。今後も、これらのテクニックを活用し、より高度なフォームバリデーションの実装を目指していきましょう。

株式会社ログラス テックブログ

Discussion