React でフォームを扱うためのライブラリである react-hook-form についてまとめました。
この記事は以下の構成になっています。
- react-hook-form とは
- react-hook-form を使わずにフォームを実装する
- react-hook-form を使う手順
- パッケージをインストール
- useForm をインポートして、useForm から必要なプロパティ、メソッドを取得する
- register でバリデーションルールとエラーメッセージを指定する
- formState でエラーメッセージを表示する
- onSubmit に送信処理を書く
- その他の使い方
- フォームの値を監視する
- フォームの値をリセットする
- UI ライブラリのコンポーネントを使う
- react-hook-form のメリット
react-hook-form とは
react-hook-form とは、React でフォームを簡単に扱うためのライブラリ。
react-hook-form を使わずにフォームを実装する
react-hook-form を使わずに、記事のタイトルと本文を入力して送信したら表示されるという機能を作る。
実装の流れは以下の通り。
- Form コンポーネントと Article コンポーネントを作る
- Form と Article の親コンポーネントで、useState を使って、フォームに入力する値である title ・ content と、送信されて表示される値である articleTitle ・ articleContent を管理する
onChange
を使ってフォームに入力できるようにするonSubmit
を使ってフォームを送信できるようにする- 送信された値を表示する
1. Form コンポーネントと Article コンポーネントを作る
記事のタイトルを入力する input と本文を入力する textarea をもつ Form コンポーネントを作る。
Form.jsx
export const Form = () => { return ( <> <form style={{ textAlign: 'center' }}> <div> <label htmlFor='title'>タイトル</label> <input type='text' id='title' /> </div> <div> <label htmlFor='content'>本文</label> <textarea id='content' rows={4} cols={40} /> </div> <div> <input type='submit' value='送信' /> </div> </form> </> ) }
次に、送信された値を表示する Article コンポーネントを作る。
Article.jsx
export const Article = () => { return ( <div style={{ textAlign: 'center' }}> <p>タイトル:</p> <p>本文:</p> </div> ) }
これらの Form と Article を親のコンポーネントで呼び出す。
App.jsx
import { Form } from './Form' import { Article } from './Article' function App() { return ( <> <Form /> <Article /> </> ) } export default App
現状は JSX を表示するだけ。
2. Form と Article の親コンポーネントで、useState を使って、フォームに入力する値である title ・ content と、送信されて表示される値である articleTitle ・ articleContent を管理する
次に App コンポーネントで、useState を使って、フォームに入力するタイトルである title と本文である content、送信された値である articleTitle と articleContent の state を管理する。
App.jsx
import { useState } from 'react' import { Form } from './Form' import { Article } from './Article' function App() { const [title, setTitle] = useState('') const [content, setContent] = useState('') const [articleTitle, setArticleTitle] = useState('') const [articleContent, setArticleContent] = useState('') return ( <> <Form title={title} content={content} /> <Article /> </> ) } export default App
Form の props として title と content を渡して、input と textarea の value 属性でそれぞれの値を表示されるようにする。
Form.jsx
export const Form = () => { return ( <> <form style={{ textAlign: 'center' }}> <div> <label htmlFor='title'>タイトル</label> <input type='text' id='title' value={title} /> </div> <div> <label htmlFor='content'>本文</label> <textarea id='content' rows={4} cols={40} value={content} /> </div> <div> <input type='submit' value='送信' /> </div> </form> </> ) }
title と content は初期値のままなので現状は空文字で何も表示されない。
3. onChange
を使ってフォームに入力できるようにする
次に Form コンポーネントで、onChange
を使ってフォームに入力できるようにする。
onChange
は子コンポーネントである Form で発火して state を変更するが、state を更新する関数は親コンポーネントにあるため、親の App.jsx でsetTitle
とsetContent
を使った関数を作り、props として Form に渡して、onChange
で実行されるようにする。
App.jsx
import './App.css' import { Counter } from './Counter' import { AuthProvider } from './AuthContext' import { Form } from './Form' import { useState } from 'react' import { Article } from './Article' function App() { const [title, setTitle] = useState('') const [content, setContent] = useState('') const [articleTitle, setArticleTitle] = useState('') const [articleContent, setArticleContent] = useState('') const handleChangeTitle = (e) => { setTitle(e.target.value) } const handleChangeContent = (e) => { setContent(e.target.value) } return ( <> <Form title={title} content={content} onChangeTitle={handleChangeTitle} onChangeContent={handleChangeContent} /> <Article /> </> ) } export default App
Form.jsx
export const Form = ({ title, content, onChangeTitle, onChangeContent }) => { return ( <> <form style={{ textAlign: 'center' }}> <div> <label htmlFor='title'>タイトル</label> <input type='text' id='title' onChange={onChangeTitle} value={title} /> </div> <div> <label htmlFor='content'>本文</label> <textarea id='content' rows={4} cols={40} onChange={onChangeContent} value={content} /> </div> <div> <input type='submit' value='送信' /> </div> </form> </> ) }
これでフォームに入力できるようになる。
4. onSubmit
を使ってフォームを送信できるようにする
次に入力された値を送信する。
送信は Form コンポーネントでのonSubmit
で行われるので、onChange
同様、処理を関数として App から Form に渡しておく必要がある。
App.jsx
import { useState } from 'react' import { Form } from './Form' import { Article } from './Article' function App() { const [title, setTitle] = useState('') const [content, setContent] = useState('') const [articleTitle, setArticleTitle] = useState('') const [articleContent, setArticleContent] = useState('') const handleChangeTitle = (e) => { setTitle(e.target.value) } const handleChangeContent = (e) => { setContent(e.target.value) } const handleSubmit = (e) => { e.preventDefault() setArticleTitle(title) setArticleContent(content) setTitle('') setContent('') } return ( <> <Form title={title} content={content} onChangeTitle={handleChangeTitle} onChangeContent={handleChangeContent} onSubmit={handleSubmit} /> <Article /> </> ) } export default App
送信処理はhandleSubmit
で実装する。処理の内容は以下の通り。
- フォームが送信されるとデフォルトの動作としてページがリロードされるため、
e.preventDefault()
でデフォルトの動作が起こらないようにする - 送信された
title
とcontent
をsetArticleTitle
とsetArticleContent
でarticleTitle
とarticleContent
に保存する - フォームの値が必要ないので
setTitle
とsetContent
で初期化する
Form
export const Form = ({ title, content, onChangeTitle, onChangeContent, onSubmit, }) => { return ( <> <form onSubmit={onSubmit} style={{ textAlign: 'center' }}> <div> <label htmlFor='title'>タイトル</label> <input type='text' id='title' onChange={onChangeTitle} value={title} /> </div> <div> <label htmlFor='content'>本文</label> <textarea id='content' rows={4} cols={40} onChange={onChangeContent} value={content} /> </div> <div> <input type='submit' value='送信' /> </div> </form> </> ) }
onSubmit
イベントに props のonSubmit
を渡す。
ここまででフォームの入力から送信までが完成。
5. 送信された値を表示する
最後にarticleTitle
とarticleContent
を props として Article コンポーネントに渡して表示する。
App.jsx
import { useState } from 'react' import { Form } from './Form' import { Article } from './Article' function App() { const [title, setTitle] = useState('') const [content, setContent] = useState('') const [articleTitle, setArticleTitle] = useState('') const [articleContent, setArticleContent] = useState('') const handleChangeTitle = (e) => { setTitle(e.target.value) } const handleChangeContent = (e) => { setContent(e.target.value) } const handleSubmit = (e) => { e.preventDefault() setArticleTitle(title) setArticleContent(content) setTitle('') setContent('') } return ( <> <Form title={title} content={content} onChangeTitle={handleChangeTitle} onChangeContent={handleChangeContent} onSubmit={handleSubmit} /> <Article articleTitle={articleTitle} articleContent={articleContent} /> </> ) } export default App
Article.jsx
export const Article = ({ articleTitle, articleContent }) => { return ( <div style={{ textAlign: 'center' }}> <p>タイトル:{articleTitle}</p> <p>本文:{articleContent}</p> </div> ) }
title
とcontent
を Article に渡して表示するとフォームの入力がリアルタイムで表示されるので、articleTitle
とarticleContent
に保存して渡している。
これでフォームの入力から送信して表示するまでの処理が完成。
react-hook-form を使う手順
次にこのフォームを react-hook-form を使って実装して違いを理解する。
react-hook-form での基本的はフォームの実装は以下の手順で行う。
- パッケージをインストール
- useForm をインポートして、useForm から必要なプロパティ、メソッドを取得する
- register で個々のフィールドを作成する
- formState でエラーメッセージを表示する
- onSubmit に送信処理を書く
1. パッケージをインストール
react-hook-form を使うにはパッケージが必要なので、以下のコマンドを実行してreact-hook-form
をインストールする。
npm i react-hook-form
2. useForm をインポートして、useForm から必要なメソッド等を取得する
react-hook-form ではuseForm
というカスタムフックを使ってフォームを実装する。
useForm
によってフォームの状態管理やバリデーションのルールを指定できる。
フォームのコンポーネントは HookForm とする。
import { useForm } from 'react-hook-form' export const HookForm = () => { const { register, handleSubmit, formState: { errors }, } = useForm() return <form></form> }
useForm から取得したのは以下。
register
- input などに入力された値を扱う関数(バリデーションなども指定できる)handleSubmit
- フォームの送信処理を行う関数formState
- フォームの状態を管理するプロパティformState: { errors }
- バリデーションのエラーメッセージ
3. register 関数でそれぞれのフィールドを作成する
react-hook-form では register
を使って個々のフォームを作成する。
register
には第一引数にフォームの name 属性を指定し、第二引数はそのフォームに対するバリデーションルールを設定する。
import { useForm } from 'react-hook-form' export const HookForm = () => { const { register, handleSubmit, formState: { errors }, } = useForm() return ( <form style={{ textAlign: 'center' }}> <div> <label htmlFor='title'>タイトル</label> <input type='text' id='title' {...register('title', { required: 'タイトルは必須です' })} /> </div> <div> <label htmlFor='content'>本文</label> <textarea id='content' rows={4} cols={40} {...register('content', { required: '本文は必須です' })} /> </div> <div> <input type='submit' value='送信' /> </div> </form> ) }
ここではタイトルと本文を入力するフォームを実装し、入力必須のバリデーションを設定している。
4. formState でエラーメッセージを表示する
formState
の errors
を使って、エラーメッセージを表示する。
import { useForm } from 'react-hook-form' export const HookForm = () => { const { register, handleSubmit, formState: { errors }, } = useForm() return ( <form style={{ textAlign: 'center' }}> <div> <label htmlFor='title'>タイトル</label> <input type='text' id='title' {...register('title', { required: 'タイトルは必須です' })} /> {errors.title && <p>{errors.title.message}</p>} </div> <div> <label htmlFor='content'>本文</label> <textarea id='content' rows={4} cols={40} {...register('content', { required: '本文は必須です' })} /> {errors.content && <p>{errors.content.message}</p>} </div> <div> <input type='submit' value='送信' /> </div> </form> ) }
5. onSubmit 関数を作り、onSubmit イベントで実行する
react-hook-form では form の onSubmit
でフォームの送信処理を行う。onSubmit
が発火した時に、useForm からインポートした handleSubmit
に実際の送信処理を渡したものをイベントハンドラとして実行する。
import { useState } from 'react' import { HookForm } from './HookForm' import { Article } from './components/Article' function App() { const [article, setArticle] = useState({ title: '', content: '' }) const handleFormSubmit = (data) => { setArticle({ title: data.title, content: data.content }) } return ( <> <HookForm onSubmit={handleFormSubmit} /> <Article articleTitle={article.title} articleContent={article.content} /> </> ) } export default App
HookForm.jsx
import { useForm } from 'react-hook-form' export const HookForm = ({ onSubmit }) => { const { register, handleSubmit, formState: { errors }, } = useForm() return ( <form onSubmit={handleSubmit(onSubmit)} style={{ textAlign: 'center' }}> <div> <label htmlFor='title'>タイトル</label> <input type='text' id='title' {...register('title', { required: 'タイトルは必須です' })} /> {errors.title && <p>{errors.title.message}</p>} </div> <div> <label htmlFor='content'>本文</label> <textarea id='content' rows={4} cols={40} {...register('content', { required: '本文は必須です' })} /> {errors.content && <p>{errors.content.message}</p>} </div> <div> <input type='submit' value='送信' /> </div> </form> ) }
handleSubmit
によってバリデーションが行われる。
バリデーションが成功すると、送信されたフォームのデータが onSubmit
に渡され、onSubmit
が実行される。
その他
フォームの値をリアルタイムで表示する
プレビューのような、フォームの値をリアルタイムで表示するにはwatch
を使う。
先ほどの例のコードをwatch
を使ってフォームの入力と同時にリアルタイムで表示されるようにする。
import { useState } from 'react' import { useForm } from 'react-hook-form' import { Article } from './components/Article' export const HookForm = ({ onSubmit }) => { const [article, setArticle] = useState({ title: '', content: '' }) const { register, handleSubmit, formState: { errors }, watch, } = useForm() const title = watch('title') const content = watch('content') const handleFormSubmit = (data) => { setArticle({ title: data.title, content: data.content }) } return ( <> <form onSubmit={handleSubmit(handleFormSubmit)} style={{ textAlign: 'center' }} > <div> <label htmlFor='title'>タイトル</label> <input type='text' id='title' {...register('title', { required: 'タイトルは必須です' })} /> {errors.title && <p>{errors.title.message}</p>} </div> <div> <label htmlFor='content'>本文</label> <textarea id='content' rows={4} cols={40} {...register('content', { required: '本文は必須です' })} /> {errors.content && <p>{errors.content.message}</p>} </div> <div> <input type='submit' value='送信' /> </div> </form> <Article title={title} content={content} /> </> ) }
Article コンポーネントを HookForm と同階層に表示し、watch
で監視している値を props として渡す。
フォームの送信処理は意味がなくなるが、フォームの入力を別の場所でリアルタイムに表示できる。
フォームの値を初期化する
フォームの値を初期化するにはreset
を使う。
例のフォームで送信処理を行った後にreset
でフォームの値を初期化する。
import { useForm } from 'react-hook-form' import { Article } from './components/Article' import { useState } from 'react' export const HookForm = () => { const [article, setArticle] = useState({ title: '', content: '' }) const { register, handleSubmit, formState: { errors }, reset, } = useForm() const handleFormSubmit = (data) => { setArticle({ title: data.title, content: data.content }) reset() } return ( <> <form onSubmit={handleSubmit(handleFormSubmit)} style={{ textAlign: 'center' }} > <div> <label htmlFor='title'>タイトル</label> <input type='text' id='title' {...register('title', { required: 'タイトルは必須です' })} /> {errors.title && <p>{errors.title.message}</p>} </div> <div> <label htmlFor='content'>本文</label> <textarea id='content' rows={4} cols={40} {...register('content', { required: '本文は必須です' })} /> {errors.content && <p>{errors.content.message}</p>} </div> <div> <input type='submit' value='送信' /> </div> </form> <Article title={article.title} content={article.content} /> </> ) }
handleFormSubmit
内で、 を実行した後に、reset
を実行することでフォームの値を初期化する。
UI ライブラリのコンポーネントを使う場合
外部の UI ライブラリのコンポーネントを使用するときに、register
が対応していない場合はController
を使う。
Controller
は外部の UI ライブラリのコンポーネントを react-hook-form に統合するためのコンポーネント。
Controller
を使う際は useForm からcontrol
を取得してController
で指定する必要がある。control
はフォームのフィールドを管理するオブジェクトで、値やバリデーションなどのフォームの状態を追跡する。
例として MaterialUI からフォームのコンポーネントを使うとする。(MaterialUI はインストールしてある前提)
import { useState } from 'react' import { useForm, Controller } from 'react-hook-form' import { TextField, Button } from '@mui/material' import { Article } from './components/Article' export const HookForm = () => { const [article, setArticle] = useState({ title: '', content: '' }) const { handleSubmit, formState: { errors }, reset, control, } = useForm() const handleFormSubmit = (data) => { setArticle({ title: data.title, content: data.content }) reset() // フォーム送信後にリセット } return ( <> <form onSubmit={handleSubmit(handleFormSubmit)} style={{ textAlign: 'center' }} > <div> <Controller name='title' control={control} defaultValue='' rules={{ required: 'タイトルは必須です' }} render={({ field }) => ( <TextField {...field} label='タイトル' variant='outlined' error={!!errors.title} helperText={errors.title ? errors.title.message : ''} fullWidth margin='normal' /> )} /> </div> <div> <Controller name='content' control={control} defaultValue='' rules={{ required: '本文は必須です' }} render={({ field }) => ( <TextField {...field} label='本文' variant='outlined' multiline rows={4} error={!!errors.content} helperText={errors.content ? errors.content.message : ''} fullWidth margin='normal' /> )} /> </div> <div> <Button variant='contained' color='primary' type='submit'> 送信 </Button> </div> </form> <Article title={article.title} content={article.content} /> </> ) }
Controller
で指定していることは以下。
name
- name 属性を指定control
- useForm の control を指定defaultValue
- 初期値を指定rules
- バリデーションルールを指定render
- 実際に描画するコンポーネントを指定
render
にはfield
というオブジェクトを引数に受け取ってコンポーネントを返すコールバック関数を渡している。
field
はフォームフィールドの値やイベントハンドラなどが含まれる react-hook-form が提供するオブジェクト。
react-hook-form のメリット
react-hook-form を使うと、主に以下のメリットがある。
- フォームのデータを簡単に管理できる
- 不要な再レンダリングを防げる
- バリデーションを簡単に書ける
フォームのデータを簡単に管理できる
react-hook-form を使わない場合は、useState を使ってフォームごとに state を作って、onChange のイベントハンドラで入力できるようにするが、react-hook-form を使う場合は register
を使ってフォームを実装し、 handleSubmit
で送信処理を書くだけ。
不要な再レンダリングを防げる
react-hook-form を使わない場合は、onChange によってテキストを 1 文字入力するたびに再レンダリングが発生するが、react-hook-form を使う場合は必要な時にフォームの状態状態が更新されるので余分な再レンダリングは発生しない。
バリデーションを簡単に書ける
react-hook-form を使わない場合は、バリデーションのメッセージも useState で管理する必要があるが、react-hook-form を使う場合は、バリデーションのルールを register で宣言的に書くことができ、エラーメッセージは formState
の errors
で表示できる。