はじめに
学習記録アプリにフォーム入力機能を追加するにあたり、バリデーション(入力チェック)をどう実装するか悩みました。
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が内部でonChange、onBlur、ref、nameなどの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の数が減る:
useStateでtitle、time、titleError、timeError...と4つ以上必要だったのが、useForm1つで済む -
バリデーションロジックがinputの近くにある:
registerの引数にルールを書くので、「このinputにはどんなチェックがあるか」がすぐわかる -
送信時のif文が不要:
handleSubmitがバリデーション通過を保証してくれるので、onSubmitの中ではバリデーションを気にしなくていい
特に3つ目が大きくて、onSubmit関数がシンプルになるのは可読性の面で助かりました。
終わりに
react-hook-formは「フォームの値管理」と「バリデーション」という2つの面倒な処理を、registerとhandleSubmitというシンプルなAPIでまとめてくれるライブラリでした。
小さなフォームでも導入する価値があると感じたので、今後のプロジェクトでも使っていきたいと思います。