LoginSignup
7

posted at

updated at

Organization

React Hook FormとYupで作る型安全なフォーム

yup: 1.0.0-beta.8 を使用しています。

はじめに

React Hook FormとYupは、Reactでフォームを作る際によく使われるライブラリの組み合わせです。これらを使用することで、高機能なフォームを簡単に実装することができます。

この記事では、以下の事を記述します。

  1. React Hook Form と Yup の概要
  2. React Hook Form と Yup のデータ処理の流れ
  3. 各ユースケースごとの実装例

実装例のサンプルコードは以下のリポジトリにも公開していますので、参考にしてください。

React Hook Form とは

まず、 React Hook Form がどういう物で何を解決する物なのかを説明します。
React Hook FormはReactアプリケーション内でのフォーム管理を簡単にするためのライブラリです。
このライブラリは、以下のような問題を解決することを目的としています。

  • フォームのステート管理: React Hook Formによって、入力フィールドの値を管理することができます。これにより、アプリケーション内でのフォームステート管理が簡単になります。
  • バリデーション: React Hook Formは、入力値を検証するためのバリデーションロジックを簡単に実装できます。これにより、入力値の正当性を確認することができます。
  • エラーメッセージの表示: React Hook Formは、入力値が不正である場合にエラーメッセージを表示することができます。
  • コードの簡潔性: React Hook Formは、フォームの管理と操作を簡潔なコードで実現することができます。これにより、コードの読みやすさとメンテナンス性が向上します。

Yup とは

次にYupがどういう物で何を解決する物なのかを説明します。
YupはJavaScriptのバリデーションフレームワークです。このフレームワークは、以下のような問題を解決することを目的としています。

  • 統一性: Yupは、バリデーションロジックを統一して実装することができます。これにより、アプリケーション内でのバリデーションロジックの一貫性が保たれます。
  • 簡潔性: Yupは、バリデーションロジックを簡潔なコードで実現することができます。これにより、コードの読みやすさとメンテナンス性が向上します。
  • 高機能: Yupは、様々なバリデーションルールを簡単に実装することができます。例えば、文字列の長さや数値の範囲など、多様なバリデーションルールを提供します。
  • 実用性: Yupは、業界標準的なバリデーションフレームワークとして実用的であり、多くのアプリケーション開発者が採用しています。

React Hook Form と Yup のデータ処理の流れ

React Hook FormとYupの概要を説明した後、次はこれらのライブラリでフォームデータがどのように処理されるかを見ていくことにします。
以下は、フォーム内のデータの流れを示す概要図です。

この図を元に、各フローを解説します。

フォーム入力前

まず最初に、編集画面などでフォームの初期値が存在する場合、React Hook Formのフックを呼び出す際に「DefaultValue」オプションを指定します。

実際のコードだとこのような形になります。

const {
    handleSubmit,
    register,
    formState: { errors },
} = useForm<Schema>({
    resolver: yupResolver(schema, { strict: true }),
    defaultValues: {
      number: 1,
    },
});

フォーム入力中

React Hook Formは非制御型フォームライブラリであり、input要素が信頼できる情報源(truth of source)となります。
そのため、通常のフックが値を管理するのではなく、React Hook Formは各フォーム要素からrefを取得して、フォーム内の値を知ることとなります。

一般的には制御型コンポーネントが推奨されていますので、この形式はあまり見ないかもしれません。
しかし、この一般的では無い方式を採用することにより、React Hook Formは他のフォームライブラリよりも高いパフォーマンスを保持しています。

制御型・非制御型の違いについては、別の記事などを参照すると理解しやすいと思います。
https://zenn.dev/nekoniki/articles/6102d68097e59a

フォーム送信時

ユーザーが操作してsubmitボタンを押した際に、useFormは各フォームからデータを引っ張ってきてそれを元にバリデーションルールによってテストが走ります。

もし、 yup と連携した場合は、ここで yup による value の検証が行われる事となります。

// value は useForm から提供される
// (コード上には無いが yupResolver が内部的に行っている処理)
const data = await userSchema.validate(value);

この際に、 validate が失敗すると error になり onSubmit は呼び出されません。

validate が成功すると yup を通ったデータが onSubmit に渡される事となります。
onSubmit 関数では、 API に通信してデータ更新などを行う事が出来ます。

ユースケース別実装例

ここからは、一般的なフォームの要素別にソースコードを見ていきます。

text input 要素

React Hook FormとYupを使って、入力必須のテキスト入力のフォームを作成します。

動作

具体的な動作は以下のようになります。
空文字を入力しようとすると、

image.png

このようにエラー表示が表示されます。

image.png

テキストの入力がされていれば、

image.png

このようにコンソールに出力されます。
(実際にはAPIに送信したりなどを行います)

image.png

コード

コードを以下に示します。

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

type RequestBody = {
  text: string;
};

const schema = yup.object().shape({
  text: yup.string().required("Text is required"),
});

interface Schema extends yup.InferType<typeof schema> {}

export default function TextInput() {
  const {
    handleSubmit,
    register,
    formState: { errors },
  } = useForm<Schema>({
    resolver: yupResolver(schema),
  });

  const onSubmit = (data: Schema) => {
    const submitData: RequestBody = {
      text: data.text,
    };

    console.log(submitData);
  };

  return (
    <>
      <main>
        <form onSubmit={handleSubmit(onSubmit)}>
          <input {...register("text")} />
          {errors.text && <p>{errors.text.message}</p>}
          <button type="submit">Submit</button>
        </form>
      </main>
    </>
  );
}

コードの解説をしていきます。

1.モジュールのインポート:

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

フォーム状態を扱うための関数と yup のバリデーションと統合するための関数がインポートされています。

  • react-hook-form: React の Hooks API でフォームの入力状態を扱うためのライブラリ。
  • yup: JavaScript のオブジェクトのバリデーションライブラリ。
  • @hookform/resolvers/yup: react-hook-form と yup を一緒に使うためのライブラリ。

2.RequestBody 型定義:

type RequestBody = {
  text: string;
};

API リクエストで送信するボディのデータ構造を定義した型です。

この型定義は、通常、GraphQL Codegen や Aspida などによって生成されますが、サンプルコードの為にこのコンポーネント内で宣言しています。

3.yup スキーマ定義:

const schema = yup.object().shape({
  text: yup.string().required("Text is required"),
});

このコードは、Yup ライブラリを使用して、入力内容を検証するためのスキーマを定義しています。

このスキーマでは、「text」が必須項目であることを確認しています。期待する値が入力されない場合はエラーを返します。

4.inferType 関数を使って、Schema インターフェースを定義

interface Schema extends yup.InferType<typeof schema> {}

このコードは、yup の inferType を使って、Schema の型定義をしています。

yup.InferType は、yup スキーマオブジェクトから TypeScript の型情報を生成するために使用されます。ここでは、先ほど定義した schema 変数を渡して、Schema の型定義をしています。

この Schema の型は後で useForm フックに使用されます。

5.useFormの使用します:

const {
    handleSubmit,
    register,
    formState: { errors },
  } = useForm<Schema>({
    resolver: yupResolver(schema),
  });

このコードは、React Hooks の useForm フックを使用して、フォームを制御するための変数を取得しています。

6.onSubmit関数を宣言します:

const onSubmit = (data: Schema) => {
    const submitData: RequestBody = {
      text: data.text,
    };
    
    console.log(submitData);
};

フォームが正常に送信されたときに呼ばれる関数です。Yup によってバリデーションが行われ、バリデーション済みのデータが引数に渡されます。
console.logでの出力が行われていますが、実際には API 向けにフォームデータを加工して送信します。

7.Form 要素

return (
    <>
      <main>
        <form onSubmit={handleSubmit(onSubmit)}>
          <input {...register("text")} />
          {errors.text && <p>{errors.text.message}</p>}
          <button type="submit">Submit</button>
        </form>
      </main>
    </>
  );
  • handleSubmit 関数を form 要素の onSubmit イベントハンドラに設定しています。
  • register 関数を呼び出して、input 要素に名前を登録しています。
  • errors オブジェクトから text プロパティを取得して、エラーがあればメッセージを表示しています。

number input 要素

React Hook FormとYupを使って、数字入力のフォームを作成します。

動作

具体的な動作は以下のようになります。
テキストを入力しようとすると、

image.png

このようにエラー表示が表示されます。

image.png

数字の入力がされていれば、

image.png

このようにコンソールに出力されます。
(実際にはAPIに送信したりなどを行います)

image.png

コード

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

type RequestBody = {
  number: number;
};

const schema = yup.object().shape({
  number: yup.number().typeError("Must be a number").required(),
});

interface Schema extends yup.InferType<typeof schema> {}

export default function NumberInput() {
  const {
    handleSubmit,
    register,
    formState: { errors },
  } = useForm<Schema>({
    resolver: yupResolver(schema),
  });

  const onSubmit = (data: Schema) => {
    const submitData: RequestBody = {
      number: data.number,
    };

    console.log(submitData);
  };

  return (
    <>
      <main>
        <form onSubmit={handleSubmit(onSubmit)}>
          <input {...register("number")} />
          {errors.number && <p>{errors.number.message}</p>}
          <button type="submit">Submit</button>
        </form>
      </main>
    </>
  );
}

基本的な部分は同じですが、以下のスキーマ定義が text input と異なっています。

const schema = yup.object().shape({
  number: yup.number().typeError("Must be a number").required(),
});

select 要素

React Hook FormとYupを使って、セレクト要素のフォームを作成します。

動作

具体的な動作は以下のようになります。
未選択の状態で確定しようとすると、

image.png

このようにエラー表示が表示されます。

image.png

選択がされていれば、

image.png

このようにコンソールに出力されます。
(実際にはAPIに送信したりなどを行います)

image.png

コード

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

type SelectOptions = "one" | "two" | "three";

type RequestBody = {
  select: SelectOptions;
};

const schema = yup.object().shape({
  select: yup
    .string()
    .oneOf(["one", "two", "three"] as const)
    .defined(),
});

interface Schema extends yup.InferType<typeof schema> {}

export default function Select() {
  const {
    handleSubmit,
    register,
    formState: { errors },
  } = useForm<Schema>({
    resolver: yupResolver(schema),
  });

  const onSubmit = (data: Schema) => {
    const submitData: RequestBody = {
      select: data.select,
    };

    console.log(submitData);
  };

  return (
    <>
      <main>
        <form onSubmit={handleSubmit(onSubmit)}>
          <select {...register("select")} defaultValue="">
            <option value="" disabled>
              Please Select
            </option>
            <option value="one">One</option>
            <option value="two">Two</option>
            <option value="three">Three</option>
          </select>
          {errors.select && <p>{errors.select.message}</p>}
          <button type="submit">Submit</button>
        </form>
      </main>
    </>
  );
}

基本的な部分は同じですが、以下のスキーマ定義が text input と異なっています。

const schema = yup.object().shape({
  select: yup
    .string()
    .oneOf(["one", "two", "three"] as const)
    .defined(),
});

select プロパティは、string 型であり、かつ、配列 ["one", "two", "three"] の要素の一つとして定義されている必要があることを意味しています。
また、yup.defined() メソッドは、このプロパティが必須であることを意味しています。

select 要素 (必須で無い場合)

React Hook FormとYupを使って、セレクト要素のフォームを作成します。
ただし、今回は未選択の場合も送信できるとします。

動作

具体的な動作は以下のようになります。
未選択の状態で確定とすると、

image.png

このようにnullが出力されます。

image.png

コード

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

type SelectOptions = "one" | "two" | "three";

type RequestBody = {
  select: SelectOptions | null;
};

const schema = yup.object().shape({
  select: yup
    .string()
    .oneOf(["", "one", "two", "three"] as const)
    .defined(),
});

interface Schema extends yup.InferType<typeof schema> {}

export default function Select() {
  const {
    handleSubmit,
    register,
    formState: { errors },
  } = useForm<Schema>({
    resolver: yupResolver(schema),
  });

  const onSubmit = (data: Schema) => {
    const submitData: RequestBody = {
      select: data.select === "" ? null : data.select,
    };

    console.log(submitData);
  };

  return (
    <>
      <main>
        <form onSubmit={handleSubmit(onSubmit)}>
          <select {...register("select")} defaultValue="">
            <option value="" disabled>
              Please Select
            </option>
            <option value="one">One</option>
            <option value="two">Two</option>
            <option value="three">Three</option>
          </select>
          {errors.select && <p>{errors.select.message}</p>}
          <button type="submit">Submit</button>
        </form>
      </main>
    </>
  );
}

基本的な部分は同じですが、スキーマ定義とonSubmit関数が必須の場合と異なっています。
以下が今回のスキーマ定義です。

const schema = yup.object().shape({
  select: yup
    .string()
    .oneOf(["", "one", "two", "three"] as const)
    .defined(),
});

スキーマ定義では、未選択時の文字列として「""」が追加されました。
select要素のoptionを参照すると、未選択時には、valueに""が指定されています。このため、未選択時でもバリデーションを通過することができます。

<select {...register("select")} defaultValue="">
    <option value="" disabled>
      Please Select
    </option>
    <option value="one">One</option>
    <option value="two">Two</option>
    <option value="three">Three</option>
</select>

また、onSubmit 関数では、空文字列を null に変換することでサーバーの API 定義に合わせています。以下のように記述されています。

const onSubmit = (data: Schema) => {
    const submitData: RequestBody = {
      select: data.select === "" ? null : data.select,
    };
    
    console.log(submitData);
};

終わりに

今回紹介した yup の使い方を理解することで、React でのフォームバリデーションが更に簡単になります。
このような技術を知っていることで、より安全なアプリケーションを作ることができると考えられます。
それでは、次回もお楽しみに!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
7