React Hook Formは、Reactアプリケーションでのフォームバリデーションを簡単かつ効率的に実現するためのライブラリです。単にinput要素の値を取得するだけでなく、高機能なバリデーション機能も備えており、煩雑になりがちなフォーム作成の作業を大幅に簡略化できます。

特に、「フォームの作成が苦手」「複雑なバリデーションを実装するのが面倒」という方にとって、React Hook Formは大きな助けとなるでしょう。

ZodはTypeScriptとの相性もいいことからReact Hook Form以外の場所でも利用されるケースが増えています。
fukidashi

本文書ではすぐに実践で活用できるようにバリデーションやエラーメッセージの表示などフォームを作成する上で必要な機能を中心にシンプルなコードを利用して説明しています。

利用する React Hook Form ライブラリのバージョンは 7 です。React のバージョンは 18.2.0 です。

Reactでは他にもフォームバリデーションライブラリが存在します。本ブログでは バリデーションライブラリ Formik の設定方法についても公開しています。

React Hook Formの特徴

React Hook Formは以下のような特徴を持っています。

  1. シンプルで軽量
  • 不要な再レンダリングを最小限に抑える設計になっています。例えば、入力フィールドが変更されても、そのフィールドのみが再レンダリングされ、フォーム全体の再レンダリングは行われません。
  1. カスタマイズ可能なバリデーション
  • 組み込みのバリデーションルールが豊富です(required, min, max, pattern など)。
  • Yup, Zodなどの人気のバリデーションライブラリを利用することもできます。
  1. 再利用性の高いフォーム設計
  • フォームロジックとUIを分離できる設計になっています。
  • Controller コンポーネントを使用して、カスタムフォームコンポーネントを簡単に作成可能です。
  1. エラーハンドリング
  • フォームのエラーメッセージを簡単に設定/管理できます

ライブラリを利用しない場合

React Hook Formを使ってフォームを作成する前にライブラリを使用しない場合のフォームの作成方法について確認しておきます。確認が必要ない人はこの章をスキップしてください。

Reactでフォームを実装する際には、一般的には以下の3つの方法があります。それぞれに特徴があり、要件や好みに応じて選択が可能です。実際にコードを利用して動作確認を行いながらそれぞれの特徴を確認していきます。

  1. useState Hookを利用する方法(Controlled Component)
  2. useRefを利用する方法(Uncontrolled Component)
  3. Hookやライブラリを利用しない方法

emailとpasswordの 2 つの入力欄を持つログインフォームをApp.jsファイルに作成して動作確認を行います。プロジェクトは”create-react-app”を利用して作成しています。

useStateを利用した場合

useStateを利用した場合のコードは下記の通りです。


import './App.css';
import { useState } from 'react';

function App() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({
      email,
      password,
    });
  };

  const handleChangeEmail = (e) => {
    setEmail(e.target.value);
  };
  const handleChangePassword = (e) => {
    setPassword(e.target.value);
  };
  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" name="email" value={email} onChange={handleChangeEmail} />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input
            id="password"
            name="password"
            value={password}
            onChange={handleChangePassword}
            type="password"
          />
        </div>
        <div>
          <button type="submit">ログイン</button>
        </div>
      </form>
    </div>
  );
}

export default App;

useStateでemailとpasswordを定義して初期値は空としています。input要素に文字を入力するとhandleChange関数が実行されてemail、passwordに入力した値が保存されます。ログインボタンをクリックするとhandleSubmit関数が実行されコンソールに入力した値がオブジェクトで表示されます。input要素のvalueの値はuseState(React)によって制御されています。

ブラウザで確認すると以下のログイン画面が表示されます。

ログイン画面
ログイン画面

handleChangeEmail, handleChangePassword関数を追加せずonChangeイベントに処理を直接設定することもできます。


<form onSubmit={handleSubmit}>
  <div>
    <label htmlFor="email">Email</label>
    <input id="email" name="email" value={email} onChange={(e) => setEmail(e.target.vallue)} />
  </div>
  <div>
    <label htmlFor="password">パスワード</label>
    <input
      id="password"
      name="password"
      value={password}
      onChange={(e) => setPassword(e.target.vallue)}
      type="password"
    />
  </div>
  <div>
    <button type="submit">ログイン</button>
  </div>
</form>

またuseStateでオブジェクトの形でフォームの入力値を保存することもできます。eventオブジェクトのtargetに含まれるnameとvalueの値を利用して定義したformDataの値を更新します。


import './App.css';
import { useState } from 'react';

function App() {
  const [formData, setFormData] = useState({ email: '', password: '' });

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(formData);
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({ ...formData, [name]: value });
  };
  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email</label>
          <input
            id="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
          />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input
            id="password"
            name="password"
            value={formData.password}
            onChange={handleChange}
            type="password"
          />
        </div>
        <div>
          <button type="submit">ログイン</button>
        </div>
      </form>
    </div>
  );
}

export default App;

useRefを利用した場合

useStateで作成したログインフォームをuseRef Hookを利用して書き換えます。useRefはDOMを参照して要素を直接操作する場合と参照を利用してuseStateのように値を保持する場合に活用できるHookです。ここではinput要素を参照してinput要素から値を取り出します。

useRefを利用してemailRefとpasswordRefを定義して、input要素にはref属性でemailRefとpasswordRefを設定します。emailRefとpasswordRefを通してinput要素に直接アクセスを行い値を取得することができます。


import './App.css';
import { useRef } from 'react';

function App() {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({
      emai: emailRef.current.value,
      password: passwordRef.current.value,
    });
  };

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" name="email" ref={emailRef} />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input id="password" name="password" ref={passwordRef} type="password" />
        </div>

        <div>
          <button type="submit">ログイン</button>
        </div>
      </form>
    </div>
  );
}

export default App;

useRef Hookを利用して作成したログイン画面とuseState Hookを利用して作成したログイン画面のUIに違いはなくどちらも入力後にログインボタンをクリックすると入力した値がオブジェクトで表示されます。

画面や出力される内容に違いはありませんがuseState Hook, useRef Hookという2つの異なる機能を用いています。ボタンをクリック後に表示されるオブジェクトは同じですが異なる機能を利用しているため内部的な処理には大きな違いがあります。useStateでは文字を入力する度に”再レンダリング(Re-render)”が行われますがuseRefの場合には”再レンダリング(Re-render)”が行われません。handleSubmit関数の下にconsole.log(‘再レンダリング’)の処理を追加することで違いを目で見て確認することができます。

useRefについては下記の文書でも説明を行っています。

Hookを利用しない場合

Hookを利用しない場合でも入力した値を取得することができます。コードは下記の通りとなりeventからinput要素を入力した値を取り出しています。


import './App.css';

function App() {
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({
      email: e.target.email.value,
      password: e.target.password.value,
    });
  };

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email</label>
          <input type="text" id="email" name="email" />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input id="password" name="password" type="password" />
        </div>
        <div>
          <button type="submit">ログイン</button>
        </div>
      </form>
    </div>
  );
}

export default App;

React Hook Formの設定

Reactでの入力フォームの作成方法の復習ができたのでここからはReact Hook Formを利用してフォームの設定を行っていきます。

ライブラリのインストール

React Hook Formライブラリのインストールはnpmコマンドで行います。


 % npm install react-hook-form

React Hook Formを利用した場合

useState HookとuseRef Hookで作成したログインフォームをReact Hook Formを利用して書き換えます。

React Hook Form という名前にHookという単語が入っている通りカスタム Hook を利用します。利用する Hook の名前は useForm です。useForm から戻されるオブジェクトは複数のプロパティを持っていますがその中から入力フォームを作成する上で必要最低限の register, handleSubmit を利用します。その他のプロパティについても本文書の中で説明を行っていきます。

useForm Hook を利用することで下記のようにコードを書き換えることができます。


import './App.css';
import { useForm } from 'react-hook-form';

function App() {
  const { register, handleSubmit } = useForm();

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

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" {...register('email')} />
        </div>
        <div>
          <label htmlFor="password">Password</label>
          <input id="password" {...register('password')} type="password" />
        </div>
        <button type="submit">ログイン</button>
      </form>
    </div>
  );
}

export default App;

ログイン画面は先ほどまでと同じで入力フォームのフィールドに入力後ログインボタンをクリックするとブラウザのコンソールに表示されるオブジェクトの内容も同じです。

React Form Hookへの書き換えを行うことができたので中身を確認していきます。最初にinput要素の”…register”関数で何を行なっているのかが気になると思うのでregister関数を確認していきましょう。register関数についてはReact Hook Formのドキュメントを確認することでどのようなものから構成されているのか理解することがきます。

ドキュメントからregister関数に引数を指定することでname(属性), ref, onBlur, onChangeが戻されることがわかります。

ドキュメントでregister関数の内容を確認
ドキュメントでregister関数の内容を確認

ドキュメントの説明を読んだだけでは register 関数の設定についてピンときていない人もいると思うので、register 関数に引数 email を指定して 戻される name, ref, onBlur, onChange で利用すると下記のように書き換えることで見慣れた形へと変わります。useRef を利用した場合と形がほとんど同じです。


const { name, ref, onChange, onBlur } = register('email');
//略
<input
  id="email"
  name={name}
  onChange={onChange}
  onBlur={onBlur}
  ref={ref}
/>

入力を行えば onChange イベント、input 要素からカーソルを外したら onBlur イベントが発火されます。ref は React Hook Form から input 要素への参照としてアクセスする際に利用されます。name は input 要素の name 属性に設定されます。

ブラウザ上に表示されるinput要素を確認するとname属性にはregister関数の引数で設定した’email’が設定されていることがわかります。
fukidashi

register の関数によって戻される値がわかり input 要素への設定が理解できたので最初は何をしているか分からなかったコードも理解が進んだと思います。以後はコード量も少ない以下の形を利用していきます。


<input id="email" {...register('email')} />

入力した値を取得することができたので次はバリデーションの設定を確認していきます。

バリデーションの設定

バリデーションは入力した値のチェックを行う機能です。例えば一般的にユーザを作成する場合にパスワードの設定が必要となります。パスワードに文字数の制限を行いたい場合にバリデーションを利用することができます。バリデーションを利用することで文字数の制限に達していない場合はサーバへ入力内容を送信する前にユーザにエラーメッセージとして伝えることができます。

React Hook Form のバリデーションは register 関数で設定することができます。

register 関数の第一引数には input 要素の name 属性の値を設定しましたが第二引数にはオプションを設定することができ複数のバリデーションの設定を行うことができます。バリデーションには HTML5 が持つフォーム制御の機能を利用することができます。サポートされているバリデーションのルールは下記の通りです。

  • required
  • min
  • max
  • minLength
  • maxLength
  • pattern
  • validate

一番わかりやすいrequiredを設定してみましょう。emailのみ設定を行います。


<input id="email" {...register('email', { required: true })} />

email の入力欄に何も入れていない状態でログインボタンをクリックしてもコンソールには何も表示されませんが email の入力欄に一文字でも文字を入力してログインボタンを押すとブラウザのコンソールには email と password の値がオブジェクトとして表示されます。

このことからバリデーションに失敗した場合には onSubmit 関数が実行されないということがわかります。

バリデーションの required を設定することで input 要素に入力を行っていない場合には submit が実行できないことがわかりましたがブラウザ上にはエラーメッセージが表示されないため何が行われているのかがわかりません。何が行われているかわかるようにバリデーションに失敗した場合のエラーの表示方法を確認します。

email の input 要素にエラー処理を追加したコードは下記の通りです。


import './App.css';
import { useForm } from 'react-hook-form';

function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

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

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" {...register('email', { required: true })} />
          {errors.email && <div>入力が必須の項目です</div>}
        </div>
        <div>
          <label htmlFor="password">Password</label>
          <input id="password" {...register('password')} type="password" />
        </div>
        <button type="submit">ログイン</button>
      </form>
    </div>
  );
}

export default App;

エラーの表示

バリデーションのエラーを表示するためにはuseForm Hookから戻されるformStateを利用します。formStateはオブジェクトなのでエラーだけではなくフォームに関する情報を持っています。その情報の中の一つにエラー情報を持つerrorsがあります。

ここではformStateの中のerrorsだけに注目するので下記のように設定を行います。


const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm();

emailに関するエラーが発生した場合にはerrors.emailオブジェクトにtype, message, refが保存されます。


{type: 'required', message: '', ref: input#email}
message: ""
ref: input#email
type: "required"

errors.emailを利用してエラーメッセージの表示を設定します。エラーがない場合はerrors.emailはundefinedとなるためerrors.emailが値を持つ場合のみエラーメッセージを表示させます。


<form onSubmit={handleSubmit(onSubmit)}>
  <div>
    <label htmlFor="email">Email</label>
    <input id="email" {...register('email', { required: true })} />
    {errors.email && <div>入力が必須の項目です</div>}
  </div>

設定後にemailの入力欄が空の状態でログインボタンをクリックするとエラーメッセージが表示されるようになります。

エラーメッセージの表示
エラーメッセージの表示

formStateのerrorsを利用することで簡単にバリデーションエラーを表示できるようになりました。

エラーメッセージの設定

再度、エラーが発生した時のerrors.emailオブジェクトの中身を確認するとmessageプロパティがありますが中身が空であることがわかります。


{type: 'required', message: '', ref: input#email}
message: ""
ref: input#email
type: "required"

messageプロパティに値を設定したい場合にはregister関数のオプションで設定したrequiredの値をtrueから文字列に変更することでmessageプロパティにその文字列を設定することができます。


<input
  id="email"
  {...register('email', { required: '入力が必須の項目です。' })}
/>

エラーが発生するとerrors.emailオブジェクトのmessageプロパティに設定した文字列が保存されます。


{type: 'required', message: '入力が必須の項目です。', ref: input#email}
message: "入力が必須の項目です。"
ref: input#email
type: "required"

ブラウザ上に表示させたい場合には以下のように設定することができます。emailオブジェクトが存在してmessageが空でない場合に設定した場合に設定したエラーメッセージが表示されます。


<form onSubmit={handleSubmit(onSubmit)}>
  <div>
    <label htmlFor="email">Email</label>
    <input
      id="email"
      {...register('email', { required: '入力が必須の項目です。' })}
    />
    {errors.email?.message && <div>{errors.email.message}</div>}
  </div>

エラーメッセージについてはrequiredの値をstringではなくオブジェクトで設定することでも可能です。他のバリデーションのルールではオブジェクトの指定方法を利用します。


<input
  id="email"
  {...register('email', {
    required: {
      value: true,
      message: '入力が必須の項目です。',
    },
  })}
/>

複数のバリデーションの設定

required以外の複数のバリデーションを設定した場合の動作確認を行います。入力内容に文字制限がある場合にはバリデーションのminLengthやmaxLengthのルールを利用することができます。

passwordを利用してminLengthの設定を行います。バリデーションにはrequired, minLengthの2つを設定しています。minLengthのvalueに8を設定したので8文字以上入力しないとminLengthのバリデーションに失敗します。


<div>
  <label htmlFor="password">Password</label>
  <input
    id="password"
    {...register('password', {
      required: {
        value: true,
        message: '入力が必須の項目です。',
      },
      minLength: {
        value: 8,
        message: '8文字以上入力してください。',
      },
    })}
    type="password"
  />
</div>

minLengthのバリデーションに失敗した場合にはmessageを設定しているのでerrors.passwordオブジェクトには以下の情報が保存されます。requiredとは異なり、typeがrequiredではなくminLengthになっていることがわかります。


{type: 'minLength', message: '8文字以上入力してください。', ref: input#password}
message: "8文字以上入力してください。"
ref: input#password
type: "minLength"

typeはバリデーションのルールによって異なることがわかったのでtypeを利用して表示するメッセージの内容を設定することもできます。

typeの値を利用することでregister関数のオプションのmessageを利用せず表示するメッセージを設定することができます。


{errors.password?.type === 'required' && (
  <div>入力が必須の項目です。</div>
)}
{errors.password?.type === 'minLength' && (
  <div>8文字以上入力してください。</div>
)}

エラーメッセージを表示することはできましたがデフォルトの設定では1つの入力項目に対して1つのエラー情報しか保持しません。複数のバリデーションエラーを取得するためにはuseFormの引数でcriteriaModeの設定を行う必要があります。デフォルトではcriteriaModeの値は”firstError”なので”all”に変更します。


  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    criteriaMode: 'all',
  });

criteriaModeの動作確認のため複数のバリデーションエラーを発生させるためにバリデーションルールのpatternを追加します。patternでは正規表現を利用することができます。下記ではアルファベットの入力のみバリデーションをパスするので数字や記号を入力するとバリデーションに失敗します。


<input
  id="password"
  {...register('password', {
    required: {
      value: true,
      message: '入力が必須の項目です。',
    },
    pattern: {
      value: /^[A-Za-z]+$/,
      message: 'アルファベットのみ入力してください。',
    },
    minLength: {
      value: 8,
      message: '8文字以上入力してください。',
    },
  })}
  type="password"
/>

passwordに数字を入力するとerrors.passwordの中身は下記のようになります。これまではmessage, ref, typeの3つのプロパティでしたが新たにtypesのプロパティが追加され2つのバリデーションエラーの情報が保存されています。


password:
 message: "8文字以上入力してください。"
 ref: input#password
 type: "minLength"
 types:
   minLength: "8文字以上入力してください。"
   pattern: "アルファベットのみ入力してください。"

複数のバリデーションエラーを表示したい場合は下記のように行うことができます。


{
  errors.password?.types?.required && (
    <div>{errors.password.types.required}</div>
  )
}
{
  errors.password?.types?.pattern && <div>{errors.password.types.pattern}</div>
}
{
  errors.password?.types?.minLength && <div>8文字以上入力してください。</div>
}

ブラウザ上でPasswordの入力欄に数字を入れて”ログイン”ボタンをクリックすると失敗したバリデーションエラーの情報が表示されます。

複数のエラーメッセージの表示
複数のエラーメッセージの表示

初期値の設定

useForm Hookの引数にdefalutValuesを利用することでinput要素に初期値を設定することができます。


const {
  register,
  handleSubmit,
} = useForm({ defaultValues: { email: 'john@test.com', password: 'pass' } });

ブラウザで確認すると設定した初期値が入力された状態で表示されます。

初期値を設定する
初期値を設定する

バリデーションのタイミング

デフォルトの設定ではログインボタンをクリックとバリデーションが行われます。1回バリデーションが実行された後は文字を入力する度にバリデーションが実行されます。

どのタイミングでバリデーションを実行するかはuseFormの2つのオプションmode, reValidateModeによって制御することができます。また手動でバリデーションを行う方法もあります。

modeの設定

ログインボタンをクリックするとバリデーションが実行されますがこれはmodeオプションによって制御されています。デフォルトの値は”onSubmit”でonBlur, onChange, onTouched, allに変更することができます。説明をしなくても値の名前からどのような動作になるか想像できるかと思いますがmodeの値を”onSubmit”から”onChange”に変更しどのような変化があるか確認します。


const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm({
  mode: 'onChange',
  criteriaMode: 'all',
});

“onChange”はonChangeイベントを利用しているため文字を入力する度にバリデーションが実行されます。

useState HookとonChangeイベントを利用した場合は文字を入力する度に再描写が行われていましたがmodeがonChangeの場合は文字の入力の度に再描写が行われるわけではなく再描写が行われるタイミングはバリデーションのメッセージの表示/非表示が切り替わる瞬間です。

“onBlur”に変更すると入力欄からカーソルを外すとバリデーションが実行されます。

reValidateModeの設定

デフォルトの設定ではログインボタンをクリックとバリデーションが行われ、1回バリデーションが実行された後は文字を入力する度にバリデーションが実行されます。2回目からのバリデーションのタイミングを設定するのが”reValidateMode”です。デフォルトでは”onChange”が設定されており、onBlur、onSubmitに変更することができます。


const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm({
  reValidateMode: 'onSubmit',
  criteriaMode: 'all',
});

値をonSubmitに変更するとログインボタンをクリックした時のみバリデーションが行われます。

手動でのバリデーション

useFormから戻されるオブジェクトのプロパティの1つであるtriggerを利用することでバリデーションを手動で行うことができます。


const {
  register,
  handleSubmit,
  formState: { errors },
  trigger,
} = useForm({
  criteriaMode: 'all',
});
//略
<button type="submit">ログイン</button>
<button type="button" onClick={() => trigger()}>
  バリデーション
</button>

バリデーションボタンをクリックするとバリデーションが実行されます。triggerの引数に何も設定しない場合にはすべての入力項目でバリデーションが実行されます。もしpasswordのみバリデーションを行いたい場合はtrigger(‘password’)と設定することでpasswordのみバリデーションが実行されます。

mode, reValidateModeとtriggerを利用してバリデーションのタイミングを制御できることがわかりました。

dirtyの設定

エラーメッセージを表示するために formState オブジェクトの errors を利用しましたが errors プロパティ以外にもフォームで利用できる重要な情報が含まれています。dirty の設定は isDirty と dirtyFields を利用して行うことができます。dirty はフォームの入力欄に入力した内容ではなく入力欄にアクセスを行い何か文字を入力をしたかどうかをチェックする際に利用することができます。

isDirty の設定

formState オブジェクトのプロパティの一つに isDirty プロパティがあります。isDirty プロパティを利用することでユーザが入力フォームのいずれかの入力欄にフォーカスを当てて外したか文字を 1 文字でも入力しかたどうか確認することができます。デフォルトでは isDirty の値は false になっていますがフォーム中のいずれかの入力欄にフォーカスを当てて文字を 1 文字でも入力すると isDirty の値は false から true にかわります。

例えばページを開いた直後、ログインの button 要素の disable 属性に isDirty を設定することでボタンをクリックすることができせん(無効)が Email または Password の入力欄にフォーカスを当てて文字を入力すると isDirty が true から false になりログインボタンがクリックできるようになります。


function App() {
  const {
    register,
    handleSubmit,
    formState: { isDirty, errors },
  } = useForm({
    criteriaMode: 'all',
  });

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

  return (
    <div className="App">
//略
        <button type="submit" disabled={!isDirty}>
          ログイン
        </button>
      </form>
    </div>
  );
}

export default App;

ブラウザで動作確認を行うと説明した通り、ページを開いた瞬間はログインボタンをクリックすることができません。ログインボタンをクリックするためには Email か Password の文字を入力する必要があります。

isDirtyを利用してボタンをdisableに
isDirtyを利用してボタンをdisableに

デフォルト値の設定

useForm でデフォルト値を設定するとデフォルト値と異なる値を入力した場合に isDirty の値が true になります。再度 isDirty の値を false 戻したい場合にはデフォルト値と異なる値を入力する必要があります。一度 false なっても入力した内容をデフォルト値に戻すと isDirty の値は true になります。

下記のようにデフォルト値を空白にした場合は、email と password の入力欄がどちらも空白の場合はログインボタンは無効のままです。email と password のどちらかに文字を入力するとログインボタンがクリックできるようになります。


function App() {
  const {
    register,
    handleSubmit,
    formState: { isDirty, errors },
  } = useForm({
    defaultValues: { email: '', password: '' },
    criteriaMode: 'all',
  });
  //略

dirtyFieldsの設定

isDirty プロパティはフォーム全体で入力が行われているかどうかチェックを行なっていましたが、dirtyFields では入力欄(フィールド)毎に dirty のチェックを行うことができます。

email の入力欄に入力が行われたかどうかは dirtyFields.email で確認することができます。email のデフォルト値を設定している場合、email の入力欄にデフォルト値と異なる文字を入力した場合のみボタンをクリックすることができます。isDirty とは異なり、password 入力欄の影響は受けません。


function App() {
  const {
    register,
    handleSubmit,
    formState: { isDirty, errors },
  } = useForm({
    defaultValues: { email: '', password: '' },
    criteriaMode: 'all',
  });
  //略
<button type="submit" disabled="{!dirtyFields.email}">ログイン</button>

その他formStateのプロパティ

formStateのプロパティにはerrors, isDirty, dirtyFields以外にも以下のようなプロパティがあります。

  • touchedFields
  • isSubmitted
  • isSubmitSuccessful
  • isSubmitting
  • submitCount
  • isValid
  • isValidating

submitCountなどsubmitを実行した回数も取得することができますがここではバリデーションに失敗していないか確認できるisValidの動作確認を行います。デフォルトはfalseですがすべてのバリデーションをパスしてエラーがなければtrueになります。

バリデーションがパスした場合のみボタンをクリックできるように以下の設定を行います。


function App() {
  const {
    register,
    handleSubmit,
    formState: { isValid, errors },
  } = useForm({
    defaultValues: { email: '', password: '' },
    criteriaMode: 'all',
  });

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

  return (
    <div className="App">
      <h1>ログイン</h1>
//略
        <button type="submit" disabled={!isDirty || !isValid}>
          ログイン
        </button>
      </form>
    </div>
  );
}

export default App;

入力途中にバリデーションをパスするとログインボタンがクリックできるようになります。一度バリデーションにパスしてログインボタンを押せるようになっても再度バリデーションをパスできない入力値になるとログインボタンがクリックできなくなります。

入力した値を表示

入力した値を表示するための方法がいくつか準備されていますがここではuseFormから戻されるオブジェクトに含まれるgetValuesとwatchを確認していきます。watchは値を取得することができますが取得というよりもinput要素の入力を監視し、値が更新されるとその変更を即座に検知することができます。

どちらも入力した値を取得できますがwatchを利用した場合は入力が行われる毎に再描写が行われます。

getValuesによる取得

getValuesは入力フォームで設定したすべての値を取得することもできますが個別の値も取得することができます。

emailに入力した値を取得するためにフォームの中に以下のコードを追加します。emailの入力欄に入力を行っても何も表示されません。(useFormのmodeはデフォルトのonSubmit)


<div>{getValues('email')}</div>

getValuesの値を表示したい場合には再描写が必要となります。デフォルトでは再描写されるのはログインボタンをクリックした時なので入力が完了してログインボタンをクリックすると入力した値が画面に表示されます。

watchによる取得

watchもgetValuesと同様にすべての値を取得することもできますが個別の値も取得することができます。

emailに入力した値を取得するためにフォームの中に以下のコードを追加します。再描写することで値が表示されたgetValuesも一緒に設定を行っておきます。


<div>{watch('email')}</div>
<div>{getValues('email')}</div>

getValues単独の場合とは異なり、emailに文字を入力する度に入力した値が表示されます。watchにより再描写が行われるのでgetValluesの値も更新されwatch, getValuesどちらも入力した値がリアルタイムで表示されます。

watchを利用した例がドキュメントに掲載されているので利用方法を確認しておきます。watchを利用してshowAgeの入力値を監視します。watchの第二引数には初期値を設定することができるのでfalseと設定しています。watchShowAgeにはageの値が保存されinputのcheckboxをチェックすると値がfalseからtrueに変わり、チェックした時のみinput要素が表示されます。


import './App.css';
import { useForm } from 'react-hook-form';

const App = () => {
  const {
    register,
    watch,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const watchShowAge = watch('showAge', false);

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

  return (
    <div className="App">
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <input type="checkbox" {...register('showAge')} />
        </div>

        {watchShowAge && (
          <div>
            <input type="number" {...register('age', { min: 50 })} />
            {errors.age && <div>50以上を入力してください</div>}
          </div>
        )}

        <div>
          <button type="submit">送信</button>
        </div>
      </form>
    </div>
  );
};

export default App;
チェックボックスの値をwatchで監視
チェックボックスの値をwatchで監視

チェックボックスにチェックをするとinput要素が表示されます。

値が更新されることでinput要素が表示
値が更新されることでinput要素が表示

ここまでの動作確認を終えた後、ドキュメント上での説明を確認するとwatchがどのようなものか理解することができます。

This method will watch specified inputs and return their values. It is useful to render input value and for determining what to render by condition.

このメソッドは特定のiputを監視しその値を戻します。inputの値を表示したり、条件によって表示させる内容を決めるのに役に立ちます。

watch+validateの設定

ユーザを登録する際にpasswordとconfirm passwordを入力してもらいたい場合があります。その場合にwatchとvalidateを利用することで実現することができます。


import './App.css';
import { useForm } from 'react-hook-form';

function App() {
  const {
    register,
    watch,
    handleSubmit,
    formState: { errors },
  } = useForm();

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

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" {...register('email', { required: true })} />
          {errors.email && <div>入力が必須の項目です</div>}
        </div>
        <div>
          <label htmlFor="password">Password</label>
          <input
            id="password"
            {...register('password', { required: true })}
            type="password"
          />
          {errors.password && <div>入力が必須の項目です</div>}
        </div>
        <div>
          <label htmlFor="confirmPassword">Confirm Password</label>
          <input
            id="confirmPassword"
            {...register('confirmPassword', {
              validate: (val) => {
                if (!val) {
                  return '入力が必須の項目です';
                } else if (watch('password') !== val) {
                  return 'パスワードが一致していません';
                }
              },
            })}
            type="password"
          />
          {errors.confirmPassword && (
            <div>{errors.confirmPassword.message}</div>
          )}
        </div>
        <button type="submit">ログイン</button>
      </form>
    </div>
  );
}

export default App;

password, confirmPasswordに入力した値が一致しない場合は設定したエラーメッセージ”パスワードが一致していません”が表示されます。これでユーザが設定したパスワードを間違えて入力する確率が下がるかもしれません。

バリデーションライブラリYupの利用

バリデーションの動作確認ではReact Form Hookが持っているバリデーションを利用していましたが外部のバリデーションライブラリを利用することができます。利用できるライブラリにはYup, ZodなどがありますがここではYupを利用します。YupはReact Form Hook専用のライブラリではありません。

React Form HookでYupを利用するためには2つのライブラリのインストールを行う必要があります。


 % npm install @hookform/resolvers yup

Yupではスキーマの定義を行います。スキーマを利用してバリデーションの設定を行います。設定も難しいものではなくバリデーションを行いたいプロパティに対してバリデーションルールを設定していきます。下記のスキーマではemailフィールドに対して文字列、メールアドレスの形式、必須という3つのバリデーションを設定し、passwordフィールドに対して文字列、必須という2つのバリデーションでチェックしています。このように複数のバリデーションルールをチェーンのように繋げて記述することができます。


import * as yup from 'yup';

const schema = yup.object({
  email: yup.string().email().required(),
  password: yup.string().required(),
});

React Hook Formで定義したschemaを利用するためにuseForm Hookの引数でyupResolverを利用します。ここまででYupの設定は完了です。


const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm({
  resolver: yupResolver(schema),
});

バリデーションエラーを表示するための設定を加えると以下のコードとなります。


import './App.css';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

const schema = yup.object({
  email: yup.string().email().required(),
  password: yup.string().required(),
});

function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: yupResolver(schema),
  });

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

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" {...register('email', { required: true })} />
          <p>{errors.email?.message}</p>
        </div>
        <div>
          <label htmlFor="password">Password</label>
          <input id="password" {...register('password')} type="password" />
          <p>{errors.password?.message}</p>
        </div>
        <button type="submit">ログイン</button>
      </form>
    </div>
  );
}

export default App;

実際に動作確認を行うと以下のようにバリデーションに失敗した場合にはエラーが表示されます。

yupを利用したバリデーションエラー
yupを利用したバリデーションエラー

パスワードの文字数を制限したい場合にはmin, maxのルールを利用することができます。8文字以上32文字以下の文字列に設定する必要があります。


const schema = yup.object({
  email: yup.string().email().required(),
  password: yup.string().min(8).max(32),
});

日本語のエラーメッセージ

日本語のエラーメッセージを表示させたい場合は以下のように設定することができます。


const schema = yup.object({
  email: yup
    .string()
    .email('メールアドレスの形式ではありません。')
    .required('入力必須の項目です。'),
  password: yup
    .string()
    .min(8, '8文字以上入力してください。')
    .max(32, '32文字以下を入力してください。'),
});

その他のバリデーションのルールについてはyupのgithubページで公開されています。

バリデーションライブラリZodの利用

先ほどはバリデーションライブラリのYupを利用しましたがここでは別のバリデーションライブラリの一つZodを利用します。ZodはReact Form Hook専用のライブラリではなくTypeScriptと相性がよくTypeScriptでも利用されるライブラリです。

React Form HookでZodを利用するためには2つのライブラリのインストールを行う必要があります。Yupで@hookform/resolversをインストール済みの場合はzodのみインストールを行ってください。


 % npm install @hookform/resolvers zod

ZodでもYupと同様にスキーマの定義を行います。スキーマを利用してバリデーションの設定を行います。設定も難しいものではなくバリデーションを行いたいプロパティに対してバリデーションルールを設定していきます。下記のスキーマではemailフィールドに対して文字列、メールアドレスの形式、1文字以上という3つのバリデーションを設定し、passwordフィールドに対して文字列、1文字以上という2つのバリデーションでチェックしています。このように複数のバリデーションルールをチェーンのように繋げて記述することができます。


import * as z from 'zod';

const schema = z.object({
  email: z.string().email().min(1),
  password: z.string().min(1),
});

React Hook Formで定義したschemaを利用するためにuseForm Hookの引数でzodResolverを利用します。ここまででZodの設定は完了です。


const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm({
  resolver: zodResolver(schema),
});

バリデーションエラーを表示するための設定を加えると以下のコードとなります。


import './App.css';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';

const schema = z.object({
  email: z.string().email().min(1),
  password: z.string().min(1),
});

function App() {
const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm({
  resolver: zodResolver(schema),
});

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

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" {...register('email', { required: true })} />
          <p>{errors.email?.message}</p>
        </div>
        <div>
          <label htmlFor="password">Password</label>
          <input id="password" {...register('password')} type="password" />
          <p>{errors.password?.message}</p>
        </div>
        <button type="submit">ログイン</button>
      </form>
    </div>
  );
}

export default App;

実際に動作確認を行うと以下のようにバリデーションに失敗した場合にはエラーが表示されます。

Zodを利用したバリデーションエラー
Zodを利用したバリデーションエラー

日本語のエラーメッセージ

日本語のエラーメッセージを表示させたい場合は以下のように設定することができます。


const schema = z.object({
  email: z
    .string()
    .email({ message: 'メールアドレスの形式ではありません。' })
    .min(1, { message: '1文字以上入力する必要があります。' }),
  password: z.string().min(1, { message: '1文字以上入力する必要があります。' }),
});

設定後動作確認すると日本語のメッセージが表示されます。

日本語メッセージの表示
日本語メッセージの表示

エラーが発生した場合のerrorsオブジェクトの内容は下記の形になっています。

errorsオブジェクトの中身
errorsオブジェクトの中身

その他

FormProviderの設定方法

ここまではApp.jsファイルのみを利用してフォームを作成してきましたがフォーム部分を複数のコンポーネントに分割したい場合があります。その場合にFormProviderを利用することができます。


import './App.css';
import { useForm, FormProvider } from 'react-hook-form';
import NameInput from './components/NameInput';
import PasswordInput from './components/PasswordInput';

function App() {
  const methods = useForm();

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

  return (
    <div className="App">
      <h1>ログイン</h1>
      <FormProvider {...methods}>
        <form onSubmit={methods.handleSubmit(onSubmit)}>
          <NameInput />
          <PasswordInput />
          <button type="submit">ログイン</button>
        </form>
      </FormProvider>
    </div>
  );
}

export default App;

useFormから戻されたmethodsをFormProviderに渡します。FormProviderの子コンポーネントであるNameInput, PasswordInputはuseFormContextを利用して受け取ることができます。componentsディレクトリを作成してNameInput.jsx, PasswordIput.jsxファイルを作成して以下のコードを記述します。

registerやerrorsなどはこれまでと同様に利用することができます。


import { useFormContext } from 'react-hook-form';
const NameInput = () => {
  const {
    register,
    formState: { errors },
  } = useFormContext();

  return (
    <div>
      <label htmlFor="email">Email</label>
      <input
        id="email"
        {...register('email', { required: '入力が必須の項目です。' })}
      />
      {errors.email && <div>{errors.email.message}</div>}
    </div>
  );
};

export default NameInput;

import { useFormContext } from 'react-hook-form';
const PasswordInput = () => {
  const {
    register,
    formState: { errors },
  } = useFormContext();

  return (
    <div>
      <label htmlFor="password">Password</label>
      <input
        id="password"
        {...register('password', { required: '入力が必須の項目です。' })}
        type="password"
      />
      {errors.password && <div>{errors.password.message}</div>}
    </div>
  );
};

export default PasswordInput;

FormProviderを利用することで複数のコンポーネントにフォームが分かれていても対応することができます。

本文書で説明した以外にもuseFormのオプションやuseFormから戻されるオブジェクトのプロパティはありますがReact Hook Formを利用する上で必要な基礎は理解できたかと思います。ぜひReact Hook Formを利用してフォームを作成してみてください。