[React] Formの取り扱いについて理解する！

### Reactでのフォームの取り扱い
Reactでのフォームの取り扱いについて、要点をまとめました。

---

### フォームデータの取り扱い
Reactでのフォームデータの取扱いは、大きく分けて**2パターン**あリます。
それぞれの違いを見ていきましょう。

#### 「制御コンポーネント」 を用いる方法
フォームの値を親コンポーネントで`state`として管理し、値の反映には **stateのリフトアップ** を用いる。

stateのリフトアップが理解できていない方はこちらの記事をご確認ください。

https://ja.react.dev/learn/sharing-state-between-components

- メリット
  - 常に値にアクセスできるので、ユーザーの入力中にバリデーションが可能
- デメリット
    - 入力値が更新されるたびにコンポーネントが再レンダリングされる

Reactは、親から子の**単方向データフロー** を採用しているため、基本的には制御コンポーネントを用いる方法が推奨されています。

#### 「非制御コンポーネント」 を用いる方法
仮想DOMを経由せず直接DOM要素の値にアクセスする手法
`useRef`を使用する

- メリット
  - 入力値が更新されるたびにコンポーネントが再レンダリングされないので、パフォーマンス効率が良い
- デメリット
    - ユーザーの入力中にバリデーションをすることが難しい

---

### イベントパラメータについて
Reactでは、コンポーネントでイベントを扱う際、`Event`オブジェクトではなく、React独自の `Synthetic Event` を用います。

イベントパラメータを引数に受け取る場合は、`SyntheticEvent`を継承した、特定のイベント型を指定する必要があります。

```tsx
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setFormData({ ...formData, inputValue: event.target.value });
};
// ...
<input type="text" onChange={handleChange} />
```

#### :memo: イベントハンドラと対応するデータ型
```typescript
type Props = {
  onClick: (event: React.MouseEvent<HTMLInputElement>) => void
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
  onkeypress: (event: React.KeyboardEvent<HTMLInputElement>) => void
  onBlur: (event: React.FocusEvent<HTMLInputElement>) => void
  onFocus: (event: React.FocusEvent<HTMLInputElement>) => void
  onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
  onCancel: (event: React.React.SyntheticEvent<HTMLDialogElement>) => void
}
```

---

### [サンプルコード] 制御コンポーネント

ここまで基本の理解はできたかと思います。
制御コンポーネントのサンプルコードを見ていきましょう。
<br>

**前提**
- フォーム全体のデータを親コンポーネントで`state`管理
- フォームデータを更新するためのコールバック関数を定義し、子コンポーネントのイベントとして設定

```tsx
import React, { useState } from 'react';

type FormData = {
  firstName: string;
  lastName: string;
};

const ControlledComponent: React.FC = () => {
  const [formData, setFormData] = useState<FormData>({
    firstName: '',
    lastName: '',
  });

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData((prevData) => ({
      ...prevData,
      [name]: value,
    }));
  };

  return (
    <form>
      <input
        type="text"
        name="firstName"
        value={formData.firstName}
        onChange={handleChange}
      />
      <input
        type="text"
        name="lastName"
        value={formData.lastName}
        onChange={handleChange}
      />
    </form>
  );
};
```

#### :memo: [サンプルコード] 非制御コンポーネント

- `useRef`を使用して、コンポーネントの参照を保持（仮想DOMを介さない）
- 値の取得 / 更新時は`DOM`要素を直接参照 / 更新する

```jsx
import React, { useRef } from 'react';

const UncontrolledComponent: React.FC = () => {
  const firstNameRef = useRef<HTMLInputElement>(null);
  const lastNameRef = useRef<HTMLInputElement>(null);

  return (
    <form>
      <input type="text" name="firstName" ref={firstNameRef} />
      <input type="text" name="lastName" ref={lastNameRef} />
    </form>
  );
};
```

---

### [:memo: 応用] React Hook Form
Reactにはフォームデータを扱うためのライブラリがいくつか用意されています。
デファクト・スタンダードとされているものはなく、代表的なものは以下の通りです。
- React Hook Form
- Formik
- React Final Form	

`npm trends` はこのような感じ（20240810時点）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842674/4c394a7e-6959-ceca-5bc6-f2584f9bfb1c.png)


#### React Hook Formとは？ 
**フォームヘルパー**の1つ。
複雑なフォームを扱う場合などに用いられる。
特徴として、**非制御コンポーネント**を用いて、フォームの入力データを管理します。

#### React Hook Form の初期設定

`React Hook Form` を利用するまでの基本的な流れです。

- **1. useFormをインポートして初期設定する**
    - `useForm`フックをReactから呼び出し、フォームの設定を行います
    ```tsx
    const {
        register,
        handleSubmit,
        formState: { errors, isSubmitting },
        reset
    } = useForm<ContactSchema>({
        resolver: zodResolver(ContactSchema),
        mode: "onBlur
    });
    ```

    - **`register`**: 各フォーム要素を登録するための関数
    - **`handleSubmit`**: フォームの送信時に呼ばれる関数をラップし、送信前のエラーチェックを内部で実施してくれる
    - **`reset`**: フォームの値を初期状態にリセット
    - **`mode`**: フォームのバリデーションがいつ実行されるかを制御
        - `onBlur`: フィールドがフォーカスアウトしたとき
        - `onChange`: 入力が変更されるたび
        - `onSubmit`: 送信時のみバリデーションを実行
        - `all`: 入力変更の各タイミングで全てのバリデーションを実施
    - **`resolver`**: `Zod`などのサードパーティーライブラリと連携し、定義されたスキーマに基づいたバリデーションを提供

<br>

- **2. `register`関数でフォーム要素を登録する**

入力欄やチェックボックスなど、ユーザーが操作する要素に `register` を適用することで、その要素がフォームで扱われる対象であることを示します。

```tsx
<input  {...register('username')} />
```

- 引数で指定した値（今回の場合は`username`）がセットされた、`name`属性が付与
- 戻り値に`ref`プロパティが含まれるため、非制御コンポーネントによる`DOM`管理

<br>

- **3. `handleSubmit`でのサブミット処理のハンドリング**

フォームの送信ボタンをクリックしたとき、`handleSubmit` でラップされた関数が実行され、入力された値が引数として渡されます。

```tsx
const onSubmit: SubmitHandler<UserForm> = (data) => {
    console.log(data);
};

<form action="#" onSubmit={handleSubmit(onSubmit)}>
```

ここで、`data`にはユーザーが入力した値がオブジェクト形式で渡されます。たとえば、`username`という入力フィールドに入力された値などが含まれます。

---

#### バリデーション
`react-hook-form`にはバリデーションをサポートするカスタムリゾルバが複数ある。

##### カスタムリゾルバとは？
フォームバリデーションをカスタマイズするために使用。
独自のバリデーションロジックを実装したい場合に用いられる。

##### yup
カスタムリゾルバの1つ
旬が過ぎたかもしれないが...`yup`の記事が多かったので`yup`を使用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842674/00c395af-f67c-4f25-0e30-5ab7da9c9437.png)

##### zod

**※ 2025年4月8日 追記**
`react-hook-form` + `Zod` でのフォーム構成例を記事化しました。

https://qiita.com/PenPe/items/5cfe3677a7eefd5250b2#6-%E8%A3%9C%E8%B6%B3-reacthookform--zod

##### サンプルコード
`yup`を利用するまでの基本的な流れです。

1.ㅤ`src/schema` にフォームデータのスキーマを定義したファイルの作成

`src/schema/userForm.ts`
```ts
import * as yup from 'yup';
import type { InferType } from 'yup';
import { sex, userRole } from '@/constants/user_form_constants';

export const userForm = yup.object({
  name: yup.string().required('名前の入力は必須です'),
  email: yup.string().required('メールアドレスの入力は必須です'),
  sex: yup.mixed().oneOf(Object.keys(sex)),
  role: yup.mixed().oneOf(Object.keys(userRole)),
  agreement: yup.boolean().oneOf([true], '利用規約の同意が必要です').required(),
});

export type UserFormSchema = InferType<typeof userForm>;
```

2.ㅤ`Form`コンポーネントにインポート
```tsx
import { yupResolver } from '@hookform/resolvers/yup';
import type { UserFormSchema } from '@/schema/userForm';
import { userForm } from '@/schema/userForm';
```

3.ㅤ`useForm`の変更
- ジェネリックにインポートしたスキーマのデータ型を指定
- 第二引数に`yupResolver`を指定
- 戻り値から`formState: { errors }` の取得

```tsx
const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<UserFormSchema>({
    defaultValues: {
      name: '',
      email: '',
      sex: 'man',
      role: 'readonly',
      agreement: false,
    },
    resolver: yupResolver(userForm),
  });
```


4.ㅤエラーメッセージの表示
- `{errors.username?.message}`

---

##### 最終的なコード 

```tsx
import { yupResolver } from '@hookform/resolvers/yup';
import { SubmitHandler, useForm } from 'react-hook-form';

import type { UserFormSchema } from '@/schema/userForm';

import { sex, userRole } from '@/constants/user_form_constants';
import { userForm } from '@/schema/userForm';

const SampleReactHookYupResolver = () => {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<UserFormSchema>({
    defaultValues: {
      name: '',
      email: '',
      sex: 'man',
      role: 'readonly',
      agreement: false,
    },
    resolver: yupResolver(userForm),
  });

  const onReset = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
    reset();
  };

  const onSubmit: SubmitHandler<UserFormSchema> = (data) => {
    console.log(data);
  };

  return (
    <>
      <form action="#" onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label htmlFor="name">氏名</label>
          <input id="name" {...register('name')} />
          <div>{errors.name?.message}</div>
        </div>
        <div>
          <label htmlFor="email">メールアドレス</label>
          <input id="email" {...register('email')} />
          <div>{errors.email?.message}</div>
        </div>
        <div>
          <label htmlFor="sex">性別</label>
          <select {...register('sex')}>
            {Object.entries(sex).map(([key, value], index) => (
              <option key={index} value={key}>
                {value}
              </option>
            ))}
          </select>
          <div>{errors.sex?.message}</div>
        </div>
        <div>
          <label htmlFor="role">ロール</label>
          <select id="role" {...register('role')}>
            {Object.entries(userRole).map(([key, value], index) => (
              <option key={index} value={key}>
                {value}
              </option>
            ))}
          </select>
          <div>{errors.role?.message}</div>
        </div>
        <div>
          <label htmlFor="agreement">利用規約</label>
          <input id="agreement" type="checkbox" {...register('agreement')} />
          <div>{errors.agreement?.message}</div>
        </div>
        <div>
          <button>Submit</button>
        </div>
        <div>
          <button onClick={onReset}>Reset</button>
        </div>
      </form>
    </>
  );
};

export default SampleReactHookYupResolver;
```

---

### まとめ
以上です。
覚えることが多いな！ って印象でした。
ユーザビリティが高いフォームを作っている皆さんに感謝😇