React Hook Formの基本的な使い方まとめ

React でフォームを扱うためのライブラリである react-hook-form についてまとめました。

この記事は以下の構成になっています。

  • react-hook-form とは
  • react-hook-form を使わずにフォームを実装する
  • react-hook-form を使う手順
    1. パッケージをインストール
    2. useForm をインポートして、useForm から必要なプロパティ、メソッドを取得する
    3. register でバリデーションルールとエラーメッセージを指定する
    4. formState でエラーメッセージを表示する
    5. onSubmit に送信処理を書く
  • その他の使い方
    • フォームの値を監視する
    • フォームの値をリセットする
    • UI ライブラリのコンポーネントを使う
  • react-hook-form のメリット

react-hook-form とは

react-hook-form とは、React でフォームを簡単に扱うためのライブラリ。

react-hook-form を使わずにフォームを実装する

react-hook-form を使わずに、記事のタイトルと本文を入力して送信したら表示されるという機能を作る。

実装の流れは以下の通り。

  1. Form コンポーネントと Article コンポーネントを作る
  2. Form と Article の親コンポーネントで、useState を使って、フォームに入力する値である title ・ content と、送信されて表示される値である articleTitle ・ articleContent を管理する
  3. onChangeを使ってフォームに入力できるようにする
  4. onSubmitを使ってフォームを送信できるようにする
  5. 送信された値を表示する

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 でsetTitlesetContentを使った関数を作り、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で実装する。処理の内容は以下の通り。

  1. フォームが送信されるとデフォルトの動作としてページがリロードされるため、e.preventDefault()でデフォルトの動作が起こらないようにする
  2. 送信されたtitlecontentsetArticleTitlesetArticleContentarticleTitlearticleContentに保存する
  3. フォームの値が必要ないのでsetTitlesetContentで初期化する

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. 送信された値を表示する

最後にarticleTitlearticleContentを 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>
  )
}

titlecontentを Article に渡して表示するとフォームの入力がリアルタイムで表示されるので、articleTitlearticleContentに保存して渡している。

これでフォームの入力から送信して表示するまでの処理が完成。

react-hook-form を使う手順

次にこのフォームを react-hook-form を使って実装して違いを理解する。

react-hook-form での基本的はフォームの実装は以下の手順で行う。

  1. パッケージをインストール
  2. useForm をインポートして、useForm から必要なプロパティ、メソッドを取得する
  3. register で個々のフィールドを作成する
  4. formState でエラーメッセージを表示する
  5. 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 でエラーメッセージを表示する

formStateerrors を使って、エラーメッセージを表示する。

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 で宣言的に書くことができ、エラーメッセージは formStateerrors で表示できる。