0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

react-hook-formでフォームバリデーションを実装してみた

0
Posted at

はじめに

学習記録アプリにフォーム入力機能を追加するにあたり、バリデーション(入力チェック)をどう実装するか悩みました。

Reactでフォームを扱う方法はいくつかありますが、今回はreact-hook-formというライブラリを使いました。この記事では、なぜreact-hook-formを選んだのか、そして実際にどう使ったのかをまとめます。

問題

学習記録アプリには「学習内容」と「学習時間」を入力するフォームがあります。以下のバリデーションが必要でした。

  • 学習内容が未入力なら「内容の入力は必須です」と表示
  • 学習時間が未入力なら「時間の入力は必須です」と表示
  • 学習時間が0未満なら「時間は0以上である必要があります」と表示

useStateだけで実装すると大変

最初はuseStateでフォームの値とエラーメッセージを管理しようとしました。

const [title, setTitle] = useState("");
const [time, setTime] = useState("");
const [titleError, setTitleError] = useState("");
const [timeError, setTimeError] = useState("");

const onSubmit = () => {
  let hasError = false;
  if (!title) {
    setTitleError("内容の入力は必須です");
    hasError = true;
  }
  if (!time) {
    setTimeError("時間の入力は必須です");
    hasError = true;
  }
  if (Number(time) < 0) {
    setTimeError("時間は0以上である必要があります");
    hasError = true;
  }
  if (hasError) return;
  // 登録処理...
};

これでも動きますが、入力項目が増えるたびにstateとバリデーションロジックが膨らんでいき、管理が大変になります。

解決方法

react-hook-formを導入する

npm install react-hook-form

useFormフックを使う

useFormフックを呼ぶだけで、フォームの状態管理とバリデーションの仕組みが手に入ります。

import { useForm } from "react-hook-form";

type FormValues = {
  title: string;
  time: string;
};

const {
  register,
  handleSubmit,
  reset,
  formState: { errors },
} = useForm<FormValues>();

ここで返ってくるものをそれぞれ説明します。

返り値 役割
register input要素に紐づけて、値の取得とバリデーションルールを設定する
handleSubmit バリデーションを通過した場合だけ送信処理を実行する
reset フォームの値を初期化する
errors バリデーションエラーの情報を持つオブジェクト

registerでバリデーションルールを設定する

registerをinput要素に展開(スプレッド)すると、そのinputがreact-hook-formの管理下に入ります。第2引数にバリデーションルールを書きます。

<Input
  placeholder="例: TypeScript"
  {...register("title", {
    required: "内容の入力は必須です",
  })}
/>
<Input
  type="number"
  placeholder="例: 2"
  {...register("time", {
    required: "時間の入力は必須です",
    min: {
      value: 0,
      message: "時間は0以上である必要があります",
    },
  })}
/>

{...register("title", { ... })}のスプレッド構文は、react-hook-formが内部でonChangeonBlurrefnameなどのpropsを生成して、inputに渡しています。これにより、自分でstateを管理する必要がなくなります。

エラーメッセージを表示する

errorsオブジェクトにバリデーション失敗時の情報が入ります。Chakra UIのFormErrorMessageと組み合わせると、以下のようにきれいに表示できます。

<FormControl isInvalid={!!errors.title}>
  <FormLabel>学習内容</FormLabel>
  <Input {...register("title", { required: "内容の入力は必須です" })} />
  <FormErrorMessage>{errors.title?.message}</FormErrorMessage>
</FormControl>

isInvalid={!!errors.title}で、エラーがあるときだけエラー表示のスタイルが適用されます。!!は値をboolean型に変換する書き方です。

handleSubmitでバリデーションを通過した場合だけ送信する

<form onSubmit={handleSubmit(onSubmit)}>

handleSubmitは、すべてのバリデーションが通った場合だけ引数の関数(onSubmit)を実行します。バリデーションに引っかかった場合はonSubmitは呼ばれず、errorsに情報がセットされます。

resetでフォームを初期化する

新規登録と編集でモーダルを共用しているため、モーダルを開くときにフォームの値を適切に設定する必要があります。

// 新規登録:空にする
const handleOpenModal = () => {
  setEditingRecord(null);
  reset({ title: "", time: "" });
  onOpen();
};

// 編集:既存の値をセットする
const handleEditModal = (record: Record) => {
  setEditingRecord(record);
  reset({ title: record.title, time: record.time });
  onOpen();
};

resetにオブジェクトを渡すと、フォームの各フィールドがその値で初期化されます。編集時は既存データを、新規登録時は空文字をセットしています。

自分で考えたこと

react-hook-formを使う前は「ライブラリを入れるほどのことか?」と思っていました。しかし実際に使ってみると、以下の点で明確にメリットがありました。

  • stateの数が減るuseStatetitletimetitleErrortimeError...と4つ以上必要だったのが、useForm1つで済む
  • バリデーションロジックがinputの近くにあるregisterの引数にルールを書くので、「このinputにはどんなチェックがあるか」がすぐわかる
  • 送信時のif文が不要handleSubmitがバリデーション通過を保証してくれるので、onSubmitの中ではバリデーションを気にしなくていい

特に3つ目が大きくて、onSubmit関数がシンプルになるのは可読性の面で助かりました。

終わりに

react-hook-formは「フォームの値管理」と「バリデーション」という2つの面倒な処理を、registerhandleSubmitというシンプルなAPIでまとめてくれるライブラリでした。

小さなフォームでも導入する価値があると感じたので、今後のプロジェクトでも使っていきたいと思います。

参考

0
0
0

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
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?