Next.js × React Hook Form で動的なフォームをつくる方法

# 目次
[1.はじめに](#1-はじめに)
[2.React Hook Form とは](#2-reacthookformとは)
[3.使用環境](#3-使用環境)
[4.基本的な使い方](#4-基本的な使い方)
[5.動的なフォームの実装方法](#5-動的なフォームの実装方法)
[6.さいごに](#6-さいごに)

# 1. はじめに
私は2023年10月より、内定直結型エンジニア学習プログラム「アプレンティス」に2期生として参加しています。

https://apprentice.jp/

現在、アプレンティスの課題としてオリジナルプロダクトの開発を行っており、その中で **React Hook Form** の使い方を学んだので、シェアしようと思います。

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

基本的な使い方と併せて、特に「追加」「削除」ボタンなどを押すことで**動的に増減する入力欄**の実装方法をまとめます。

# 2. ReactHookFormとは
React Hook Form は、その名の通り、React で Form を作るためのフックを備えたライブラリです。useForm というフックを利用して、状態管理を手軽に行えるのがメリットです。

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

また、今回テーマとする動的なフォームを作るのにも役立ちます。

なお、今回はバリデーションについては触れません。後に Zod で実装することを想定して、記載しませんのでご了承ください。
Zod についてご存じない方は下記参考サイトをご覧ください。

【参考】 React Hook Form の基本的

https://qiita.com/y-suzu/items/952d417f0853341a97df

https://reffect.co.jp/react/react-hook-form

# 3. 使用環境
MacBook Air : M2 チップ / 16 GB
Next.js : 14.1.4
React : 18.2.0
React Hook Form : ^7.51.0
Tailwind CSS : ^3.3.0

# 4. 基本的な使い方
上記２つ目の参考サイトで紹介されている基本のコードを元に、Tailwind css で簡単に見た目を整えてみました。
ここでは、さらっと基本的な使い方を説明します。

【参考】 Tailwind CSS とは

https://udemy.benesse.co.jp/development/system/tailwind-css.html

![スクリーンショット 2024-03-26 5.50.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070527/5b86b145-86e6-7e25-0cc8-5b712e29443e.png)


```react
'use client'

import { useForm } from 'react-hook-form'

const page = () => {
  const { register, handleSubmit } = useForm()

  const onSubmit = data => console.log(data)

  return (
    <div className="App container my-8">
      <h1 className="text-center mb-8 text-lg">ログイン</h1>
      <form onSubmit={handleSubmit(onSubmit)} className=" space-y-4">
        <div>
          <label htmlFor="email" className="mr-2">
            Email
          </label>
          <input id="email" {...register('email')} className="border" />
        </div>
        <div>
          <label htmlFor="password" className="mr-2">
            Password
          </label>
          <input
            id="password"
            {...register('password')}
            type="password"
            className="border"
          />
        </div>
        <button className="border block mx-auto" type="submit">
          ログイン
        </button>
      </form>
    </div>
  )
}

export default page
```

### ① ライブラリのインストール
`npm install react-hook-form`

### ② ライブラリのインポート
必ず`use client`ディレクティブをつけて、ライブラリをインポートします。
```react
'use client'

import { useForm } from 'react-hook-form'
```
`use client`を宣言しないと、以下のエラーで怒られますのでお気をつけください。
```
TypeError: (0 , react_hook_form__WEBPACK_IMPORTED_MODULE_1__.useForm) is not a function
```
![スクリーンショット 2024-03-26 5.55.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070527/b20a1957-475d-b469-f7a6-5e176c48685a.png)

### ③ register
参考サイトでは、まず最低限必要なものとして、useForm フックから以下２つのメソッドを呼び出しています。

```react
const { register, handleSubmit } = useForm()
```
まずは register について。
register はその名の通り、登録のためのメソッドです。
input などの要素を、フォームの項目として登録するために使用します。
```react
<div className="flex flex-col">
  <label htmlFor="name" className="mr-2">
    ユーザーネーム
  </label>
  <input id="name" {...register('name')} className="border" />
</div>
```
このようにして、input などの要素に引数として渡します。register の中に複数のメソッドが含まれているため、それをスプレッド構文で展開して渡しています。
`('name')`というのは、「`name`という項目名でフォームに登録する」という意味で、フォームを送信した時に下の画像のように`name`という項目名で値が送信されます。
![スクリーンショット 2024-03-26 7.20.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070527/35c8d0fa-a321-f140-8a55-e8c668f4aff8.png)
この register で登録を行わないとフォームを送信しても値が取得できないので、必須の項目です。

### ④ handleSubmit
```react
const onSubmit = data => console.log(data)

// 略

<form onSubmit={handleSubmit(onSubmit)} className=" space-y-4">
```
handleSubmit は、フォーム送信時に実行する関数を指定するためのメソッドです。上で定義した onSubmit をサブミット時に実行するように、handleSubmit に渡しています。
試しに、このメソッドを介さずに直接 onSubmit を渡してみます。
```react
<form onSubmit={onSubmit} className=" space-y-4">
```
そうすると、フォームは送信できるのですが、一瞬で消えてしまいます。
Vanila JavaScript (いわゆる普通の JavaScript)でフォームを送信する時に `e.preventdefault` を記述したことがある方はお気づきかと思いますが、フォームを送信したあとにデフォルトでリロードが走るので、その動作を止めないといけないのです。

`handleSubmit`はその辺の調整をしてくれているのと、**async** 関数を渡すことができることも特徴です。
フォームで受け取った値はバックエンドに送信することが多いと思いますが、その際に **async/await** で非同期処理することができます。
今回はただ`console.log`で出力しています。

このようにして、`register`と`handleSubmit`を使って、先程の画像のようにフォームのデータを受け取ることができます。
これが最も基本的な React Fook Form の使い方です。

# 5. 動的なフォームの実装方法
今回のトピックスのメインです。
ログインフォームでは動的に増減する入力欄を使うことが少ないと思うので、先程のコードを元にして、新規会員登録ページ仕様に書き換えてみます。

![スクリーンショット 2024-03-26 10.29.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070527/c1a45a48-b876-69d7-397b-46d22bd642bc.png)

このように、サブメールアドレスを複数登録できるようにしたい場合を考えてみます。それぞれのサブメールアドレスには、通知を受け取るかどうかのチェックボックスも配置します。
書き換えたコードがこちらです。

```react
'use client'

import { useForm } from 'react-hook-form'

const page = () => {
  const { register, handleSubmit, control } = useForm()

  const onSubmit = data => console.log(data)

  return (
    <div className="App container my-8">
      <h1 className="text-center mb-8 text-lg">新規会員登録</h1>
      <form onSubmit={handleSubmit(onSubmit)} className=" space-y-4">
        <div className="flex flex-col">
          <label htmlFor="name" className="mr-2">
            ユーザーネーム
          </label>
          <input id="name" {...register('name')} className="border" />
        </div>
        <div className="flex flex-col">
          <label htmlFor="main_email" className="mr-2">
            メールアドレス
          </label>
          <input
            id="main_email"
            {...register('main_email')}
            type="email"
            className="border"
          />
        </div>
        <div className="flex flex-col">
          <label htmlFor="sub_emails" className="mb-2">
            サブメールアドレス
            <small className="ml-2">※複数登録できます</small>
          </label>
          {fields.map((field, index) => (
            <div className="flex items-center">
              <input
                key={field.id}
                id="sub_emails"
                {...register(`sub_emails.${index}.email`)}
                type="email"
                className="border block w-64"
              />
              <label className="text-sm leading-none ml-4 mr-1">
                通知を受け取る
              </label>
              <input
                type="checkbox"
                {...register(`sub_emails.${index}.notice`)}
              />
            </div>
          ))}
        </div>
        <button
          type="button"
          className="border block mx-auto">
          アドレスを追加
        </button>
        <div className="flex flex-col">
          <label htmlFor="password" className="mr-2">
            パスワード
          </label>
          <input
            id="password"
            {...register('password')}
            type="password"
            className="border"
          />
        </div>
        <button className="border block mx-auto" type="submit">
          登録
        </button>
      </form>
    </div>
  )
}

export default page
```
ここから、動的フォームにするために追記していきます。
### ① useFieldArray
動的なフォームを作る場合に活躍するのが **useFieldArray** です。

こちらも React Fook Form からインポートしてきます。
```react
import { useForm, useFieldArray } from 'react-hook-form'
```

動的なフォームは下記のように定義します。
```react
const { fields, append, remove } = useFieldArray({
name: 'sub_emails',
control,
})
  ```
`name`の部分には、フォームに登録する、動的に増減させたい`field`名を記述します。（`field`については次で説明します。）
今回は`sub_emails`とします。

その他の項目について順に説明していきます。

### ② fiels
追加で増減できるようにしたい要素のセットをひとつの`field`と見立てます。今回増減できるようにしたいのはサブメールアドレスの`input`要素と、お知らせ通知の`input`(checkbox)なので、それらが`field`に含まれます。

後ほど設置する「追加」「削除」ボタンを押すとその`field`単位で増減し、その複数のフィールドを`fiels`という配列として扱えます。その名の通り「FieldArray」というわけです。
`fiels`が実際どのようになっているのかは、このあとの ⑥`append`のところで併せて画面で確認します。

### ③ control
この`FieldArray`を`useForm`に紐づけるためには、`useForm`の`control`オブジェクトを`useFieldArray`に渡す必要があります。
そのため、`useForm`から取り出してきます。
```react
const { register, handleSubmit, control } = useForm()
```
それを、①`useFieldArray`のコードのように`useFieldArray`に渡すことで、フォームを送信した時にこの`FieldArray`の情報も送信されるようになります。


### ④ register の登録方法
`useFieldArray`として配列でフォームに登録するためには、`fiels`を使って該当の`input`を以下のように書き換えます。
```react
<div className="flex flex-col">
  <label htmlFor="sub_emails" className="mb-2">
    サブメールアドレス
    <small className="ml-2">※複数登録できます</small>
  </label>
  {fields.map((field, index) => (
    <div className="flex items-center" key={field.id}>
      <input
        id="sub_emails"
        {...register(`sub_emails.${index}.email`)}
        type="email"
        className="border block w-64"
      />
      <label className="text-sm leading-none ml-4 mr-1">
        通知を受け取る
      </label>
      <input
        type="checkbox"
        {...register(`sub_emails.${index}.notice`)}
      />
    </div>
  ))}
</div>
```
前述のとおり`fiels`という配列として扱われるため、JSX には`fields`を`map`展開する形で記述します。
`index`も使うので引数として渡します。
```react
{fields.map((field, index) => (
```
各配列の`key`には`field.id`を指定します。
```react
<div className="flex items-center" key={field.id}>
```
`register`には、以下のように登録します。
意味としては、`sub_mails`という`fiels`の`index`番目の`email`という項目として登録するという意味になります。
```react
{...register(`sub_emails.${index}.email`)}
```
同じようにして、お知らせ通知の`input`は`notice`という名前で登録します。
```react
{...register(`sub_emails.${index}.notice`)}
```
この状態で確認すると、以下のように入力欄が消えてしまいます。
![スクリーンショット 2024-03-26 9.42.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070527/edd61302-28c6-acd2-ae77-e26754bf7e7d.png)
次の`append`を使って追加ボタンを設置すると、ボタンを押せば入力欄が出てくるのですが、最初から１つは表示してほしいですよね。
一つ目の入力欄を最初から表示させるためには、フォームの初期値を設定しておく必要があります。

### ⑤ defaultValues
初期値は、`useForm`の引数に`defaultValues`として設定します。
```react
const { register, handleSubmit, control } = useForm({
defaultValues: {
  name: '',
  main_email: '',
  sub_emails: [{ email: '', notice: true }],
  password: '',
},
})
```
他のフォーム項目についても、併せて記述しました。ここに入れた文字列が、初期値として表示されるようになります。
大事なのは`sub_mails`の部分で、`useFieldArray`を使う場合は配列の中に項目をオブジェクトで記載します。初期値がない場合でも、空文字として指定しておかないと、`useFieldArray`では先程のように入力欄自体が表示されない状態になってしまうので、指定しておく必要があります。

checkbox の方は、デフォルトでチェックが入っている状態にするために`true`にしてみました。
ここまでで、以下のように入力欄が出てくるようになります。

![スクリーンショット 2024-03-26 13.25.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070527/b97d6189-5a5a-a3ed-d4fd-68376feded6d.png)

ここまでの全体のコードはこちらです。
```react
'use client'

import { useForm, useFieldArray } from 'react-hook-form'

const page = () => {
  const { register, handleSubmit, control } = useForm({
    defaultValues: {
      name: '',
      main_email: '',
      sub_emails: [{ email: '', notice: true }],
      password: '',
    },
  })

  const { fields, append, remove } = useFieldArray({
    name: 'sub_emails',
    control,
  })

  console.log(fields)

  const onSubmit = data => console.log(data)

  return (
    <div className="App container my-8">
      <h1 className="text-center mb-8 text-lg">新規会員登録</h1>
      <form onSubmit={handleSubmit(onSubmit)} className=" space-y-4">
        <div className="flex flex-col">
          <label htmlFor="name" className="mr-2">
            ユーザーネーム
          </label>
          <input id="name" {...register('name')} className="border" />
        </div>
        <div className="flex flex-col mb-4">
          <label htmlFor="main_email" className="mr-2">
            メールアドレス
          </label>
          <input
            id="main_email"
            {...register('main_email')}
            type="email"
            className="border"
          />
        </div>
        <div className="flex flex-col">
          <label htmlFor="sub_emails" className="mb-2">
            サブメールアドレス
            <small className="ml-2">※複数登録できます</small>
          </label>
          {fields.map((field, index) => (
            <div className="flex items-center" key={field.id}>
              <input
                id="sub_emails"
                {...register(`sub_emails.${index}.email`)}
                type="email"
                className="border block w-64"
              />
              <label className="text-sm leading-none ml-4 mr-1">
                通知を受け取る
              </label>
              <input
                type="checkbox"
                {...register(`sub_emails.${index}.notice`)}
              />
            </div>
          ))}
        </div>
        <button
          type="button"
          className="border block mx-auto">
          アドレスを追加
        </button>
        <div className="flex flex-col">
          <label htmlFor="password" className="mr-2">
            パスワード
          </label>
          <input
            id="password"
            {...register('password')}
            type="password"
            className="border"
          />
        </div>
        <button className="border block mx-auto" type="submit">
          登録
        </button>
      </form>
    </div>
  )
}

export default page

```

### ⑥ append
`append`は、フィールドを増やす、つまり入力欄を増やすためのメソッドです。
`button`要素などの`onClick`イベントに登録し、引数には`field`の初期値を渡します。
```react
<button
  type="button"
  className="border block mx-auto"
  onClick={() => append({ email: '', notice: true })}>
  アドレスを追加
</button>
```
こうすることで、ボタンを押すと以下のように`field`が追加されます。

![スクリーンショット 2024-03-26 13.30.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070527/21ea8265-3609-c1d0-a828-8322015f08f5.png)

この時の fields をコンソールで見てみましょう。
```react
console.log(fields)
```
![スクリーンショット 2024-03-26 13.32.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070527/f9e04686-69eb-d1c1-c80c-79933d6dda21.png)
「アドレスを追加」ボタンを押すと、オブジェクトが一つずつ追加されているのが確認できます。
オブジェクトの中身を確認すると、自動で`id`が振られていて、その他に先ほど`register`で登録した`email`と`notice`の項目があることが分かります。

### ⑦ remove
`append`の逆で、`field`を一つ減らすためのメソッドです。`append`と違うのは、どの`field`を削除するのかを明示する必要があるため、各`field`に設置する必要があるという点です。

`append`と同じように`onClick`に登録し、`remove`の引数には`index`を渡します。
こうすることで、削除ボタンを押した時に、その`field`が削除されるようになります。

また、今回は１つ目の入力欄は削除できないようにしたいので、`index`が`0`以上の時のみボタンが表示されるようにしてみました。
```react
{fields.map((field, index) => (
<div className="flex items-center" key={field.id}>
  <input
    id="sub_emails"
    {...register(`sub_emails.${index}.email`)}
    type="email"
    className="border block w-64"
  />
  <label className="text-sm leading-none ml-4 mr-1">
    通知を受け取る
  </label>
  <input
    type="checkbox"
    {...register(`sub_emails.${index}.notice`)}
  />
  {index > 0 && (
    <button
      className="border block text-sm ml-2 px-1"
      onClick={() => remove(index)}>
      ✕
    </button>
  )}
</div>
))}
```
見た目はこのようになります。
![スクリーンショット 2024-03-26 13.50.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070527/59028f8c-c41b-72b4-aab9-dbd6255da4c6.png)


# 6. 送信結果
では、以下のようにフォームを入力して、登録してみましょう。
![スクリーンショット 2024-03-26 13.52.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070527/10fb6a81-3db3-9f43-3006-052b4194d736.png)

`onSubmit`の`consol.log`で中身が確認できます。 
![スクリーンショット 2024-03-26 13.54.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070527/91454852-7ee0-aa9b-c1c7-32a206195a55.png)

これで、`useFieldArray`を利用した動的フォームの実装ができました。

# 7. さいごに
今回は簡単な動的フォームの作り方をまとめてみました。
このような単純なフォームを作るには React Hook Fomr はとても便利なライブラリだと感じました。

ただ、今回私はオリジナルプロジェクトのレシピサイトを制作するにあたって、ハマってしまった部分もあります。
`type="file"`の`input`で画像のアップロードをして、即時に画像を表示させたいと考え、そのために`useForm`、`useFieldArray`と併せて**useRef** フックと **onChange** イベントを使おうとしました。
ですが、`useRef`と`onChange`が`useForm`と競合してなかなか思うように実装できなかったのです。
その点について、今後また別の記事にまとめたいと思います。

間違っている箇所がありましたら、ご指摘いただけますと幸いです。
お読みいただきありがとうございました。