react-hook-formで入力フォームを作る
最近、開発やコードレビューする中で、自身の知見のなさを実感しているakiyoshiです。
最近まで入力フォームを作るときに、項目の数だけ増えていくstateが当たり前だと思っていましたが、
フォーム以外のstateも加味すると、徐々にメンテナンスできる気がしなくなってきました。
※それは設計が悪いとは言われるとそうかもしれませんが・・・。
そんな中で調べてみると便利そうなreact-hook-form
なるものがあったため、試してみました。
例のごとく公式ドキュメントが一番詳しいので、実際に使うときはそちらを参照してください。
npm create vite@latest vite-form-practice -- --template react-ts
cd vite-form-practice/
npm i react-hook-form
実際に書くとこうなる
import { FieldErrors, useForm } from "react-hook-form";
type InputForm = {
name: string;
email: string;
gender: (typeof genderValue)[number]["value"];
};
const genderValue = [
{ value: 0, text: "FEMALE" },
{ value: 1, text: "MALE" },
{ value: 2, text: "OTHERS" },
] as const;
export const Form = () => {
// 使いたいAPIをインポート
const {
register,
handleSubmit,
reset,
formState: { isDirty, errors },
} = useForm<InputForm>({
// 初期値を設定
defaultValues: {
name: "vinci taro",
email: "init@example.com",
gender: 1,
},
});
// 有効なデータでsubmit押下時に実行される
const submitValidData = (data: InputForm) => {
console.log(data);
};
// 無効なデータでsubmit押下時に実行される(省略可)
const alertInvalidData = (data: FieldErrors<InputForm>) => {
console.error(data);
};
// 動作確認でログを出したかったため、reset関数をラップ
const handleReset = () => {
console.log("reset");
reset();
};
return (
<>
<h1>input form</h1>
{/* submitボタン押下で実行されるラップ関数 */}
<form onSubmit={handleSubmit(submitValidData, alertInvalidData)}>
<div>
<label>名前:</label>
{/* inputされた値をnameというキーで登録し、参照できるようにする */}
<input {...register("name")} />
</div>
<div>
<label>Email:</label>
<input
{...register("email", {
// バリデーションで必須チェックとパターンマッチングをしている
required: { value: true, message: "メッセージは必須です" },
pattern: {
value:
/^[A-Za-z0-9]{1}[A-Za-z0-9_.-]*@[A-Za-z0-9_.-]{1,}.[A-Za-z0-9]{1,}$/,
message: "メールの形式が不正です",
},
})}
/>
{/* 上記バリデーションで引っかかると、errosオブジェクトに値が入るため、それを利用している */}
{errors?.email && (
<p style={{ color: "red" }}>{errors.email.message}</p>
)}
</div>
<div>
<label>性別:</label>
<select {...register("gender", { min: 0, max: 2 })}>
{genderValue.map((item) => (
<option value={item.value} key={item.value}>
{item.text}
</option>
))}
</select>
</div>
{/* フォーム内で値の変更がない場合は、buttonを無効化している */}
<button disabled={!isDirty} type="submit">
submit
</button>
{/* フォーム内の値を初期値にリセットする */}
<button onClick={handleReset} type="button">reset</button>
</form>
</>
);
};
便利と感じたポイント
いろいろと要素が多いので、主要なところだけ書きます。(できることが多すぎる)
- defaultValues
- オブジェクトを渡すことでフォームの初期値を設定できます。
- register
- 入力された値を参照するために利用します。
第1引数ではキーとなる項目名、第2引数でバリデーションのルールとエラーメッセージを設定できます(省略可)
- 入力された値を参照するために利用します。
- handleSubmit
- 引数は2つとり、有効なデータの処理と無効なデータの処理でパターン分けができます。
- 有効なデータの場合にはフォームに入力した値(
{name: inputタグのname, email: inputタグのemail, gender: selectタグのgender};
)を引数で指定した関数(=ラップされた関数)に渡すことができます。
- errors
- reset
- フォーム内の入力値を1発で初期値に戻すことができます。ここでいう初期値は
defaultValues
で設定した値です。
- フォーム内の入力値を1発で初期値に戻すことができます。ここでいう初期値は
- isDirty
- 入力値に変更があったかどうかをbool値で持ちます
他にもバリデーションのタイミングをonChange
やonSubmit
の制御ができたり、
フォームの値を個別取得getValues
・設定setValue
できたりと、ひととおりやりたいことができるようになっています。
追加でバリデーションは他のライブラリも利用できる
useFormにresolversを渡すことで使えるようになります。
Zodとしてはやることは変わらないないので触れませんが、組み込むとこんな感じになります。
個人的には、UIコード内にバリデーションやメッセージまで書きたくないので、別ファイルに分離できるところが良いです。
※小規模なフォームならそのままでいい気もするので、さじ加減が難しいです。
- 直近やったZodの記事
// zod利用のために定義済みのschemaをimport
import { schema } from "./validate";
import { zodResolver } from "@hookform/resolvers/zod";
// ---中略---
const {
register,
handleSubmit,
reset,
formState: { isDirty, errors },
} = useForm<InputForm>({
// 初期値を設定
defaultValues: {
name: "vinci taro",
email: "init@example.com",
gender: 1,
},
resolver: zodResolver(schema), // zod利用のために追加
});
// ---中略---
<div>
<label>Email:</label>
{/* react-hook-formのバリデーションを削除 */}
<input {...register("email")} />
{errors?.email && (
<p style={{ color: "red" }}>{errors.email.message}</p>
)}
</div>
UIコンポーネントライブラリも入れてみる
今回はMUI(Material UI)を追加しましたが、コードの印象がかなり変わりますね。
フロントのコード自体読み慣れていないときには、辛かったです。
個人的には、どこがMUIで、どこがreact-hook-formかを意識して読めば、何とかなりました。
import { Controller, FieldErrors, useForm } from "react-hook-form";
import { schema } from "./validate";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Box,
Button,
FormControl,
InputLabel,
MenuItem,
Select,
TextField,
} from "@mui/material";
type InputForm = {
name: string;
email: string;
gender: (typeof genderValue)[number]["value"];
};
const genderValue = [
{ value: 0, text: "FEMALE" },
{ value: 1, text: "MALE" },
{ value: 2, text: "OTHERS" },
] as const;
export const Form = () => {
const {
handleSubmit,
reset,
control,
formState: { isDirty },
} = useForm<InputForm>({
// 初期値を設定
defaultValues: {
name: "vinci taro",
email: "init@example.com",
gender: 1,
},
resolver: zodResolver(schema),
});
// 有効なデータでsubmit押下時に実行される
const submitValidData = (data: InputForm) => {
console.log(data);
};
const handleReset = () => {
console.log("reset");
reset();
};
// 無効なデータでsubmit押下時に実行される(省略可)
const alertInvalidData = (data: FieldErrors<InputForm>) => {
console.error(data);
};
return (
<>
<h1>input form</h1>
<Box
component="form"
onSubmit={handleSubmit(submitValidData, alertInvalidData)}
>
<Controller
name="name"
control={control}
render={({ field, formState: { errors } }) => (
// https://mui.com/material-ui/react-text-field/
<TextField
{...field}
label="名前"
error={!!errors.name}
fullWidth
/>
)}
/>
<Controller
name="email"
control={control}
render={({ field, formState: { errors } }) => (
<TextField
{...field}
label="Email"
error={!!errors.email}
fullWidth
helperText={errors.email?.message as string}
/>
)}
/>
<Box>
<Controller
name="gender"
control={control}
render={({ field, formState: { errors } }) => (
// https://mui.com/material-ui/react-select/
<FormControl fullWidth error={!!errors.gender}>
<InputLabel id="gender">性別</InputLabel>
<Select {...field}>
{genderValue.map((item) => {
return (
<MenuItem value={item.value} key={item.value}>
{item.text}
</MenuItem>
);
})}
</Select>
</FormControl>
)}
/>
</Box>
{/* フォーム内で値の変更がない場合は、buttonを無効化している */}
<Button variant="contained" disabled={!isDirty} type="submit">
submit
</Button>
{/* フォーム内の値を初期値にリセットする */}
<Button variant="outlined" onClick={handleReset} type="button">
reset
</Button>
</Box>
</>
);
};
まとめ
formに関連するstateが無くなるだけでも、すっきりして助かりますね。
それだけではなく、他のライブラリとの合わせ技で使えるようになっている点もありがたいです。
ただし、1つの項目で複数の値を持つ場合はuseFieldArray
、UIライブラリを使いたい場合にはuseController
を使うなど、使いこなそうとすると一時的とはいえ、少しコストが高くなりそうな印象です。
また、すでにuseState
でフォームの値を管理しているところに、便利だからとreact-hook-form
を導入する場合はチーム内で開発方針のすり合わせを行いましょう。
下手に共存状態を作り出してしまうと、stateの管理ができなくなり、バグを生む可能性があります。
参考
導入方法からの説明もあるので、公式ドキュメントから入るとよいです。