9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

目次

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

1. はじめに

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

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

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

2. ReactHookFormとは

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

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

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

【参考】 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. 基本的な使い方

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

【参考】 Tailwind CSS とは

スクリーンショット 2024-03-26 5.50.30.png

'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ディレクティブをつけて、ライブラリをインポートします。

'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

③ register

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

const { register, handleSubmit } = useForm()

まずは register について。
register はその名の通り、登録のためのメソッドです。
input などの要素を、フォームの項目として登録するために使用します。

<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
この register で登録を行わないとフォームを送信しても値が取得できないので、必須の項目です。

④ handleSubmit

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

// 略

<form onSubmit={handleSubmit(onSubmit)} className=" space-y-4">

handleSubmit は、フォーム送信時に実行する関数を指定するためのメソッドです。上で定義した onSubmit をサブミット時に実行するように、handleSubmit に渡しています。
試しに、このメソッドを介さずに直接 onSubmit を渡してみます。

<form onSubmit={onSubmit} className=" space-y-4">

そうすると、フォームは送信できるのですが、一瞬で消えてしまいます。
Vanila JavaScript (いわゆる普通の JavaScript)でフォームを送信する時に e.preventdefault を記述したことがある方はお気づきかと思いますが、フォームを送信したあとにデフォルトでリロードが走るので、その動作を止めないといけないのです。

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

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

5. 動的なフォームの実装方法

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

スクリーンショット 2024-03-26 10.29.38.png

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

'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 からインポートしてきます。

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

動的なフォームは下記のように定義します。

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

このFieldArrayuseFormに紐づけるためには、useFormcontrolオブジェクトをuseFieldArrayに渡す必要があります。
そのため、useFormから取り出してきます。

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

それを、①useFieldArrayのコードのようにuseFieldArrayに渡すことで、フォームを送信した時にこのFieldArrayの情報も送信されるようになります。

④ register の登録方法

useFieldArrayとして配列でフォームに登録するためには、fielsを使って該当のinputを以下のように書き換えます。

<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 にはfieldsmap展開する形で記述します。
indexも使うので引数として渡します。

{fields.map((field, index) => (

各配列のkeyにはfield.idを指定します。

<div className="flex items-center" key={field.id}>

registerには、以下のように登録します。
意味としては、sub_mailsというfielsindex番目のemailという項目として登録するという意味になります。

{...register(`sub_emails.${index}.email`)}

同じようにして、お知らせ通知のinputnoticeという名前で登録します。

{...register(`sub_emails.${index}.notice`)}

この状態で確認すると、以下のように入力欄が消えてしまいます。
スクリーンショット 2024-03-26 9.42.16.png
次のappendを使って追加ボタンを設置すると、ボタンを押せば入力欄が出てくるのですが、最初から1つは表示してほしいですよね。
一つ目の入力欄を最初から表示させるためには、フォームの初期値を設定しておく必要があります。

⑤ defaultValues

初期値は、useFormの引数にdefaultValuesとして設定します。

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

ここまでの全体のコードはこちらです。

'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の初期値を渡します。

<button
  type="button"
  className="border block mx-auto"
  onClick={() => append({ email: '', notice: true })}>
  アドレスを追加
</button>

こうすることで、ボタンを押すと以下のようにfieldが追加されます。

スクリーンショット 2024-03-26 13.30.12.png

この時の fields をコンソールで見てみましょう。

console.log(fields)

スクリーンショット 2024-03-26 13.32.46.png
「アドレスを追加」ボタンを押すと、オブジェクトが一つずつ追加されているのが確認できます。
オブジェクトの中身を確認すると、自動でidが振られていて、その他に先ほどregisterで登録したemailnoticeの項目があることが分かります。

⑦ remove

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

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

また、今回は1つ目の入力欄は削除できないようにしたいので、index0以上の時のみボタンが表示されるようにしてみました。

{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

6. 送信結果

では、以下のようにフォームを入力して、登録してみましょう。
スクリーンショット 2024-03-26 13.52.51.png

onSubmitconsol.logで中身が確認できます。
スクリーンショット 2024-03-26 13.54.01.png

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

7. さいごに

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

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

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

9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?