【react-hook-form】react-hook-formを導入してみた

# はじめに
現在取り組んでいる、学習記録アプリのリニューアル作業にて登録フォームのバリデーションにreact-hook-formを導入することにしました。学んだことを備忘録としてまとめてみました。

## 実現したいこと
フォームの登録ボタンを押したときに、バリデーションを適用させたい

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3993509/51eae118-f89c-40a4-b15b-8dce78b4d881.png)


# react-hook-formとは？
https://react-hook-form.com/

**TOPページより引用（日本語訳）**
>"Performant, flexible and extensible forms with easy-to-use validation."
（高性能で柔軟性があり拡張可能なフォームで、使いやすいバリデーション機能を備えています）

Reactで**フォームを取り扱う際に使える高性能なライブラリ**と理解しました。

# 導入手順
## 1.react-hook-formのドキュメントを確認
https://react-hook-form.com/get-started#Quickstart

## 2.ライブラリをインストール
```shell
$ npm install react-hook-form
```

## 3.Quick StartのExampleを確認
**react-hook-formのドキュメントより引用**
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3993509/38fc1a26-ac64-4e87-9841-5a9628fc9858.png)

## 4.form形式へ変更
クイックスタートを確認すると、formタグを使用した構造を元に使用例が提示されていました。
私は各入力値を、useStateで設定するように実装していたため、form形式に変更することから始めました。

**formの形式に修正**

```diff_tsx:App.tsx
<Modal initialFocusRef={initialRef} isOpen={isOpen} onClose={onClose}>
            <ModalOverlay />
            <ModalContent>
+              <form onSubmit={handleSubmit(onRecordRegist)}>
                <ModalHeader>Modal Title</ModalHeader>
                <ModalCloseButton />
                <ModalBody>
                  <FormControl isRequired>
                    <FormLabel>学習内容</FormLabel>
                    <Input
                      ref={initialRef}
                      placeholder="学習内容"
                      onChange={(e: ChangeEvent<HTMLInputElement>) =>
                        setTitle(e.target.value) // useStateを使い学習内容をセット
                      }
                    />
                  </FormControl>
                  <FormControl isRequired>
                    <FormLabel>学習時間</FormLabel>
                    <NumberInput
                      defaultValue={0}
                      max={50}
                      min={0}
                      onChange={(_, valueAsNumber: number) =>
                        setTime(valueAsNumber) // useStateを使い学習時間をセット
                      }
                    >
                      <NumberInputField />
                      <NumberInputStepper>
                        <NumberIncrementStepper />
                        <NumberDecrementStepper />
                      </NumberInputStepper>
                    </NumberInput>
                  </FormControl>
                </ModalBody>

                <ModalFooter>
-                  <Button colorScheme="teal" onClick={onRecordRegist}>
+                  <Button colorScheme="teal" type="submit">
                    登録
                  </Button>
                </ModalFooter>
+              </form>
            </ModalContent>
          </Modal>
```

## 5.handleSubmitの導入

ドキュメントのexampleに倣って、`<form onSubmit={handleSubmit(onRecordRegist)}>`を記載しました。

https://react-hook-form.com/docs/useform/handlesubmit

**公式ドキュメントより引用**
>This function will receive the form data if form validation is successful.
>> 日本語訳：この関数は、フォームのバリデーションが成功した場合にフォームデータを受け取ります。

handleSubmitの引数に指定した、`関数(onRecordRegist)はバリデーションが成功した場合、
onRecordRegistを実行`します。
また、`data`にはフォームから受け取ったデータを取得できます。

useStateで設定していた処理は削除しました。

reac-hook-formを使用するために、カスタムフックの`useForm`を追加しました。
useFormを使うことで、フォームの管理が可能になります。

https://react-hook-form.com/docs/useform

```diff_tsx:App.tsx
import { ChangeEvent, useEffect, useState } from "react";
import { addStudyRecord, GetAllStudyRecords } from "./lib/study-record";
import { Record } from "./domain/record";
import { BsPencil } from "react-icons/bs";
import React from "react";
+ import { useForm, SubmitHandler, Controller } from "react-hook-form";

function App() {
  const [studyRecords, setStudyRecords] = useState<Record[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const { isOpen, onOpen, onClose } = useDisclosure();
-  const initialRef = React.useRef(null);
-  const [title, setTitle] = useState(""); // useStateを使い学習内容をセットしていた処理を削除
-  const [time, setTime] = useState(0); // useStateを使い学習時間をセットしていた処理を削除
+  const {
+    register,
+    handleSubmit,
+    formState: { errors },
+    setValue,
+    control,
+  } = useForm<Partial<Record>>()
  const getAllStudyRecords = async () => {
    const records = await GetAllStudyRecords();
    setStudyRecords(records);
    setIsLoading(false);
  };
  useEffect(() => {
    getAllStudyRecords();
  }, []);
-  const onRecordRegist = async () => {
+  const onRecordRegist: SubmitHandler<Partial<Record>> = async (data) => {
    const insertData: Partial<Record> = {};
+    insertData.title = data.title;
+    insertData.time = data.time;
-    insertData.title = title;
-    insertData.time = time;
    await addStudyRecord(insertData);
    getAllStudyRecords();
    onClose();
  };
```
## 6.registerの導入
学習内容欄（title）に`register`を導入し、バリデーションを追加しました。

https://react-hook-form.com/docs/useform/register

**公式ドキュメントより引用**
>This method allows you to register an input or select element and apply validation rules to React Hook Form. Validation rules are all based on the HTML standard and also allow for custom validation methods.
>>日本語訳：このメソッドでは、input または select 要素を登録し、React Hook Form にバリデーションルールを適用できます。バリデーションルールはすべて HTML 標準に基づいており、カスタムバリデーションも可能です。


```tsx:App.tsx
                <ModalBody>
                  <FormControl>
                    <FormLabel>学習内容</FormLabel>
                    <Input
                      {...register("title", { required: true })}
                      placeholder="学習内容"
                    />
                    {/* エラーの表示 */}
                    {errors.title?.type === "required" && (
                      <p style={{ color: "red" }}>学習内容は必須です</p>
                    )}
                  </FormControl>
             　　</ModalBody>
```

`{...register("title", { required: true })}`
* `"title"`:登録するフィールドの名前
* `{ required: true }`:必須項目のバリデーションルールを設定
* `…`：JavaScriptのスプレッド構文、registerが生成した属性を展開

```tsx
<Input
{...register("title", { required: true })}
placeholder="学習内容"
/>
{/* 実際はこのように展開されている */}
<Input
  name="title"
  onChange={[Function]}
  onBlur={[Function]}
  ref={[Function]}
  placeholder="学習内容"
/>
```



## 7.Controllerの導入

https://react-hook-form.com/docs/usecontroller/controller

勉強時間の入力欄にはChakraUIのNumberInputを使用していました。
ChakraUIのNumberInputなどの複雑なinput要素にはregisterが使えないため、`Controller`を導入しました。

`register`は`標準のHTMLフォーム要素（\<input>など）に直接適用するように設計`されていますが、ChakuraUIのNumberInputなど`複数の要素から成り立っているカスタムコンポーネント`には、registerは使用できません。

```tsx:App.tsx
<Controller
  name="time"  // フォーム内での識別名
  control={control}  // React Hook Formのコントロールオブジェクト
  rules={{ required: true, min: 0 }}  // バリデーションルール
  render={({ field }) => (  // レンダリング関数
    <NumberInput
      value={field.value}  // Controllerから受け取った値、「現在の値」(フォームの状態)
      onChange={(valueString) => {  // 「新しく入力された値」(変更イベントの引数)、変更があると文字列として受け取る
        field.onChange(parseInt(valueString));  // 文字列を数値に変換してからフォームに反映
      }}
    >
      <NumberInputField />
      {/* エラーの表示 */}
      {errors.time?.type === "required" && (
        <p style={{ color: "red" }}>学習時間は必須です</p>
      )}
      {errors.time?.type === "min" && (
        <p style={{ color: "red" }}>
          時間は0以上である必要があります
        </p>
      )}
      <NumberInputStepper>
        <NumberIncrementStepper />
        <NumberDecrementStepper />
      </NumberInputStepper>
    </NumberInput>
  )}
/>
```

`name`:登録するフィールドの名称
`rules`:必須項目、最小値=0のバリデーションルールを設定
`render`:コントローラーから受け取った値を元に、フォームに設定

# 導入完了

![バリデーション.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3993509/00b851af-aca5-47ff-906a-769619b5aef8.gif)


# おわりに
react-hook-formを使うことで、容易にバリデーション、フォームデータを取り扱えることを学びました。
ドキュメントを参照しつつ、自分のプロダクトに取り込むコツが少しずつ掴めてきたと感じています。


# 参考
https://react-hook-form.com/