目次
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 とは
'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
③ 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
という項目名で値が送信されます。
この 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
で出力しています。
このようにして、register
とhandleSubmit
を使って、先程の画像のようにフォームのデータを受け取ることができます。
これが最も基本的な React Fook Form の使い方です。
5. 動的なフォームの実装方法
今回のトピックスのメインです。
ログインフォームでは動的に増減する入力欄を使うことが少ないと思うので、先程のコードを元にして、新規会員登録ページ仕様に書き換えてみます。
このように、サブメールアドレスを複数登録できるようにしたい場合を考えてみます。それぞれのサブメールアドレスには、通知を受け取るかどうかのチェックボックスも配置します。
書き換えたコードがこちらです。
'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
このFieldArray
をuseForm
に紐づけるためには、useForm
のcontrol
オブジェクトを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 にはfields
をmap
展開する形で記述します。
index
も使うので引数として渡します。
{fields.map((field, index) => (
各配列のkey
にはfield.id
を指定します。
<div className="flex items-center" key={field.id}>
register
には、以下のように登録します。
意味としては、sub_mails
というfiels
のindex
番目のemail
という項目として登録するという意味になります。
{...register(`sub_emails.${index}.email`)}
同じようにして、お知らせ通知のinput
はnotice
という名前で登録します。
{...register(`sub_emails.${index}.notice`)}
この状態で確認すると、以下のように入力欄が消えてしまいます。
次の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
にしてみました。
ここまでで、以下のように入力欄が出てくるようになります。
ここまでの全体のコードはこちらです。
'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
が追加されます。
この時の fields をコンソールで見てみましょう。
console.log(fields)
「アドレスを追加」ボタンを押すと、オブジェクトが一つずつ追加されているのが確認できます。
オブジェクトの中身を確認すると、自動でid
が振られていて、その他に先ほどregister
で登録したemail
とnotice
の項目があることが分かります。
⑦ remove
append
の逆で、field
を一つ減らすためのメソッドです。append
と違うのは、どのfield
を削除するのかを明示する必要があるため、各field
に設置する必要があるという点です。
append
と同じようにonClick
に登録し、remove
の引数にはindex
を渡します。
こうすることで、削除ボタンを押した時に、そのfield
が削除されるようになります。
また、今回は1つ目の入力欄は削除できないようにしたいので、index
が0
以上の時のみボタンが表示されるようにしてみました。
{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>
))}
6. 送信結果
onSubmit
のconsol.log
で中身が確認できます。
これで、useFieldArray
を利用した動的フォームの実装ができました。
7. さいごに
今回は簡単な動的フォームの作り方をまとめてみました。
このような単純なフォームを作るには React Hook Fomr はとても便利なライブラリだと感じました。
ただ、今回私はオリジナルプロジェクトのレシピサイトを制作するにあたって、ハマってしまった部分もあります。
type="file"
のinput
で画像のアップロードをして、即時に画像を表示させたいと考え、そのためにuseForm
、useFieldArray
と併せてuseRef フックと onChange イベントを使おうとしました。
ですが、useRef
とonChange
がuseForm
と競合してなかなか思うように実装できなかったのです。
その点について、今後また別の記事にまとめたいと思います。
間違っている箇所がありましたら、ご指摘いただけますと幸いです。
お読みいただきありがとうございました。