React Hook Form は React 用のフォームバリデーションライブラリです。input 要素に入力した値を取得するだけではなくバリデーション機能なども備えており簡単にフォームを実装することができます。入力フォームの作成が嫌いな人もライブラリの力を借りることでフォーム作成の手間を軽減することができます。入力した値に対するバリデーションは React Hook Form 自身も備えていますがバリデーションライブラリの Zod を利用することも可能です。

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

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

React Hook Form の JavaScript 環境下での使用方法についてはすでに公開しているので TypeScript を利用しない場合は下記の文書を参考にしてください。

プロジェクトの作成

Vite を利用して React の TypesScript 環境の構築を行います。“npm create vite@latest”コマンドを実行します。プロジェクト名と framewark と variant を聞かれるので本文書ではプロジェクト名に”react-hook-form-ts”を設定し、“React”, “TypeScript”を選択しています。


 % npm create vite@latest
✔ Project name: … react-hook-form-ts
✔ Select a framework: › React
✔ Select a variant: › TypeScript

Scaffolding project in /Users/mac/Desktop/react-hook-form-ts...

Done. Now run:

  cd react-hook-form-ts
  npm install
  npm run dev

コマンド実行後に react-hook-form-ts が作成されるので移動して”npm install”コマンドを実行します。


 % cd react-hook-form-ts
 % npm install

プロジェクト内でデフォルトで設定しているスタイルを適用させないために src/main.tsx ファイルの import 文をコメントしておきます。


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
// import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

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

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

一般的にはフォームを作成する方法には 2 つあり、一つは useState Hook を利用する方法、もう一つは useRef を利用する方法です。前者は Controlled Component, 後者は Uncontrolled Component と呼ばれます。React によって input 要素の value をどのように制御するかの違いがあります。さらに Hook を利用しない場合についても記述しています。

email と password の 2 つの入力欄を持つログインフォームを/src/App.tsx ファイルに記述して動作確認を行います。

useState を利用した場合

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


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

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

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log({
      email,
      password,
    });
  };

  const handleChangeEmail = (e: React.ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value);
  };
  const handleChangePassword = (e: React.ChangeEvent<HTMLInputElement>) => {
    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={(e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log({
      email,
      password,
    });
  }}
>
  <div>
    <label htmlFor="email">Email</label>
    <input
      id="email"
      name="email"
      value={email}
      onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
        setEmail(e.target.value);
      }}
    />
  </div>
  <div>
    <label htmlFor="password">パスワード</label>
    <input
      id="password"
      name="password"
      value={password}
      onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
        setPassword(e.target.value);
      }}
      type="password"
    />
  </div>
  <div>
    <button type="submit">ログイン</button>
  </div>
</form>

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


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

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

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log(formData);
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    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 要素に直接アクセスを行い値を取得することができます。input 要素を参照するため useRef の型には HTMLInputElement を設定しています。参照する要素によって型の定義を変えます。emailRef.current の current プロパティの値が null の場合もあるので email.current の後には”?”を設定しています。


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

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

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    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 つの異なる Hook(機能)を用いています。ボタンをクリック後に表示されるオブジェクトは同じですが異なる機能を利用しているため内部的な処理には大きな違いがあります。useState では文字を入力する度に”再レンダリング(Re-render)“が行われますが useRef の場合には要素(input)自身が値を持つため”再レンダリング(Re-render)“が行われません。handleSubmit 関数の下に console.log(‘再レンダリング’)の処理を追加することで違いを目で見て確認することができます。

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

Hook を利用しない場

Hook を利用しない場合でも入力した値を取得することができます。


import './App.css';

function App() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  const target = e.target as any;
  console.log({
    email: target.email.value,
    password: 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;

form を経由して input 要素にアクセスを行い入力した値を取得しています。handleSubmit 関数の中に console.log(target)を追加して実行するとデベロッパーツールのコンソールにフォームそのものが表示されます。


<form>
  <div>
    <label for="email">Email</label
    ><input type="text" id="email" name="email" />
  </div>
  <div>
    <label for="password">パスワード</label
    ><input id="password" name="password" type="password" />
  </div>
  <div><button type="submit">ログイン</button></div>
</form>

handleSubmit 関数の中に console.log(target.email)を追加して実行すると input 要素が表示されます。これで target.email.value から入力値が取得できることが理解できたかと思います。


<input type="text" id="email" name="email">

React Hook Form の設定

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

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

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


 % npm install react-hook-form

インストール後の各種ライブラリのバーションを package.json ファイルで確認しておきます。


{
  "name": "react-hook-form-ts",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-hook-form": "^7.45.2"
  },
  "devDependencies": {
    "@types/react": "^18.2.15",
    "@types/react-dom": "^18.2.7",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "@vitejs/plugin-react": "^4.0.3",
    "eslint": "^8.45.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.3",
    "typescript": "^5.0.2",
    "vite": "^4.4.5"
  }
}

React Hook Form を利用した場合

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

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

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


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

type FormData = {
  email: string;
  password: string;
};

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

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

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={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;

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

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

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

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

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


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

type FormData = {
  email: string;
  password: string;
};

function App() {
  const { register, handleSubmit } = useForm<FormData>();
  const { name, ref, onChange, onBlur } = register('email');

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

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

export default App;

email の設定のみ register(‘email’)を利用して書き換えを行っています。password の設定は何も変更していません。

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


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

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


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

SubmitHandler

onSubmit 関数では handleSubmit で wrap することで中の関数の data の型が FormData に推論されますが form タグの onSubmit の引数の中で handleSubmit を利用すると data の型が推論されません。


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

return (
  <div className="App">
    <h1>ログイン</h1>
    <form onSubmit={handleSubmit(onSubmit)}>

上記のコードで data に正しい型を設定するために SubmitHandler を利用します。


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

type FormData = {
  email: string;
  password: string;
};

function App() {
  const { register, handleSubmit } = useForm<FormData>();
  const { name, ref, onChange, onBlur } = register('email');

  const onSubmit: SubmitHandler<FormData> = (data) => console.log(data);

設定後は data の型は FormData となります。

バリデーションの設定

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

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

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

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

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


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

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

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

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

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


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

type FormData = {
  email: string;
  password: string;
};

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

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

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={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();

//略
  console.log(errors.email);

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={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={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 を利用せず表示するメッセージを設定することができます。


<div>
  <label htmlFor="password">Password</label>
  <input
    id="password"
    {...register('password', {
      required: {
        value: true,
        message: '入力が必須の項目です。',
      },
      minLength: {
        value: 8,
        message: '8文字以上入力してください。',
      },
    })}
    type="password"
  />
  {errors.password?.type === 'required' && (
    <div>入力が必須の項目です。</div>
  )}
  {errors.password?.type === 'minLength' && (
    <div>8文字以上入力してください。</div>
  )}
</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: "アルファベットのみ入力してください。"

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


<input
  id="password"
  {...register('password', {
    required: {
      value: true,
      message: '入力が必須の項目です。',
    },
    pattern: {
      value: /^[A-Za-z]+$/,
      message: 'アルファベットのみ入力してください。',
    },
    minLength: {
      value: 8,
      message: '8文字以上入力してください。',
    },
  })}
  type="password"
/>
{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 の入力欄に数字を入れて”ログイン”ボタンをクリックすると失敗したバリデーションエラーの情報が表示されます。

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

ここまでの動作確認で App.tsx ファイルのコード全体は下記のようになっています。


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

type FormData = {
  email: string;
  password: string;
};

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

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

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={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: {
                value: true,
                message: '入力が必須の項目です。',
              },
              pattern: {
                value: /^[A-Za-z]+$/,
                message: 'アルファベットのみ入力してください。',
              },
              minLength: {
                value: 8,
                message: '8文字以上入力してください。',
              },
            })}
            type="password"
          />
          {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>
          )}
        </div>
        <button type="submit">ログイン</button>
      </form>
    </div>
  );
}

export default App;

初期値の設定

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


const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm<FormData>({
  defaultValues: { email: 'john@test.com', password: 'pass' },
  criteriaMode: 'all',
});

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

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

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

デフォルトの設定ではログインボタンをクリックするとバリデーションが行われます。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”に変更すると入力欄からカーソルを外すとバリデーションが実行されます。


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

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',
    defaultValues: { email: '', password: '' },
  });

  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: { dirtyFields, errors },
  } = useForm<FormData>({
    defaultValues: { email: '', password: '' },
    criteriaMode: 'all',
  });

//略
<button type="submit" disabled="{!dirtyFields.email}">ログイン</button>

その他 formState のプロパティ

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

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

touchedFields

touchedFileds を利用することで各 input 要素にカーソルを当てて外しかかどうかを確認することができます。touchedFileds は最初は空のオブジェクトですが、Email の入力欄にカーソルを合わせて外すと下記のように email プロパティが追加されます。


{
    "email": true
}

さらに Password の入力欄にカーソルを合わせて外すと下記のように password プロパティが追加されます。


{
    "email": true,
    "password"; true
}

isValid

バリデーションに失敗していないか確認できる isValid の動作確認を行います。デフォルトは false ですがすべてのバリデーションをパスしてエラーがなければ true になります。

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


function App() {
  const {
    register,
    handleSubmit,
    formState: { isDirty, 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;

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

submitCount

submitCount プロパティはログインボタンをクリックして実行した submit の数が保存されます。

isSubmited

isSubmitted プロパティはログインボタンをクリックすると値が false から true に変わります。

isSubmitting

isSubmitting プロパティはログインボタンをクリックして submit 処理が実行されている間 false から true になります。ボタンを submit 処理中に何度も実行させてくない場合に利用することができます。

submitCount, isSubmited, isSubmitting の値がどのように変化するか確認するためにログインボタンの上に下記のコードを追加します。


<div>
  <strong> submitCount : </strong> {JSON.stringify(submitCount)}
</div>
<div>
  <strong> isSubmitted : </strong> {JSON.stringify(isSubmitted)}
</div>
<div>
  <strong> isSubmitting : </strong> {JSON.stringify(isSubmitting)}
</div>

<button type="submit" disabled={!isDirty || !isValid}>
  ログイン
</button>

submitCount, isSubmited, isSubmitting を利用できるように useForm の設定を行います。


const {
  register,
  handleSubmit,
  formState: {
    isValid,
    isDirty,
    errors,
    isSubmitted,
    isSubmitting,
    submitCount,
  },
} = useForm<FormData>({
  defaultValues: { email: '', password: '' },
  criteriaMode: 'all',
});

isSubmitting の値の変化を確認するために遅延のコードを handle 関数に追加します。処理が完了するまでに 5 秒間待機させます。


const onSubmit = handleSubmit(async (data) => {
  await new Promise((resolve) => setTimeout(resolve, 5000));
  console.log(data);
});

バリデーションをパスするコードを入力後、ログインボタンをクリックしてください。

最初は submitCount の値は 0, isSubmitted と isSubmitting の値は false です。

実行前
実行前

ログインボタンをクリックします。

実行前
実行後

ログインボタンをクリックすると isSubmiting の値のみ true から false に変わります。5 秒経過すると isSbumitting の値が true から false に戻り、submitCount が 0 から 1, isSubmitted が false から true になります。もう一回ログインボタンをクリックすると submitCount の値は 2 になります。

reset の設定

reset 関数を利用することでフォームに入力した値や formState に含まれるプロパティの値をリセットすることができます。

Rest ボタンを追加し、ボタンをクリックすると reset 関数が実行され formState が持つプロパティにどのような変化があるか確認します。

動作確認で利用するコードは以下の通りです。reset 関数の引数にはリセット後に設定されるフォームの各フィールドの値を指定しています。useForm の defaultValues と同じ値を設定していますが別の値を設定することも可能です。Rest ボタンの type 属性には”reset”を設定しています。


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

type FormData = {
  email: string;
  password: string;
};

function App() {
  const {
    register,
    handleSubmit,
    reset,
    formState: {
      errors,
      isValid,
      isDirty,
      isSubmitted,
      submitCount,
      touchedFields,
    },
  } = useForm<FormData>({
    defaultValues: { email: '', password: '' },
  });

  const onSubmit = handleSubmit((data) => {
    console.log(data);
  });

  const handleReset = () => {
    reset({
      email: '',
      password: '',
    });
  };

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={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>isValid : {JSON.stringify(isValid)}</div>
        <div>isDirty : {JSON.stringify(isDirty)}</div>
        <div>submitCount : {JSON.stringify(submitCount)}</div>
        <div>isSubmitted : {JSON.stringify(isSubmitted)}</div>
        <div>touchedFields : {JSON.stringify(touchedFields)}</div>

        <button type="submit">Login</button>
        <button type="reset" onClick={handleReset}>
          Reset
        </button>
      </form>
    </div>
  );
}

export default App;

リセットの処理を行う前にフォームに入力を行います。入力を行うと isValid, isDirty の値は true になり、toucheFields のオブジェクトには email, password プロパティで追加され値は true となります。

Reset前の画面
Reset前の画面

Rest ボタンをクリックすると isValid, isDirty が true から false になり、touchedFields の値もリセットされていることが確認できます。submit は行われないので submitCount と isSubmitted の値は変わりません。

Rest ボタンに type 属性をつけない場合はボタンをクリックすると submit が行われるので isSubmitted の値と submitCount の値が更新されます。
リセット後の画面
リセット後の画面

リセットするためのボタンではなく Submit での処理が正常に完了した後にリセットを行いたいという場面があると思いますので onSubmit 関数の処理の中に Reset 関数を追加します。


const onSubmit = handleSubmit((data) => {
  console.log(data);
  reset({
    email: '',
    password: '',
  });
});

フォームに入力を行い”Login”ボタンをクリックします。

submitの処理にresetを追加
submitの処理にresetを追加

Rest ボタンとは異なり、Submit を行い処理がが完了しているので isSubmitted が true になり、submitCount が 1 になっています。isSubmitted と submitCount はリセットされないことがわかります。

ドキュメントには”It’s recommended to reset inside useEffect after submission.”と記載があるので useEffect 内で reset 関数を実行して何か違いがあるのか確認します。useEffect の依存関係には submit 処理が成功した場合に値が変わる”isSubmitSuccessful”を設定しています。onSubmit 関数内での reset 関数のコメントしています。


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

type FormData = {
  email: string;
  password: string;
};

function App() {
  const {
    register,
    handleSubmit,
    reset,
    formState: {
      errors,
      isValid,
      isDirty,
      isSubmitted,
      submitCount,
      touchedFields,
      isSubmitSuccessful,
    },
  } = useForm<FormData>({
    defaultValues: { email: '', password: '' },
  });

  useEffect(() => {
    reset({
      email: '',
      password: '',
    });
  }, [isSubmitSuccessful]);

  const onSubmit = handleSubmit((data) => {
    console.log(data);
    // reset({
    //   email: '',
    //   password: '',
    // });
  });

  const handleReset = () => {
    reset({
      email: '',
      password: '',
    });
  };

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={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>isValid : {JSON.stringify(isValid)}</div>
        <div>isDirty : {JSON.stringify(isDirty)}</div>
        <div>submitCount : {JSON.stringify(submitCount)}</div>
        <div>isSubmitted : {JSON.stringify(isSubmitted)}</div>
        <div>touchedFields : {JSON.stringify(touchedFields)}</div>

        <button type="submit">Login</button>
        <button type="reset" onClick={handleReset}>
          Reset
        </button>
      </form>
    </div>
  );
}

export default App;

フォームに入力を行い”Login”ボタンをクリックします。下記は実行後の画面です。

useEffectを利用した場合
useEffectを利用した場合

useEffect の中で reset 関数を実行した場合は submitCount も isSubmitted の値もリセットされていること確認できます。画面上では値が変化していないように見えるかもしれませんが一度 isSubmitted の値は true になり、isSubmitted の値は 1 になっています。もしそれらの値をリセットしたくない場合は reset 関数のオプションで設定を行うことができます。下記では submitCount, isSubmitted, touchedFields の値をリセットしない設定を行っています。


useEffect(() => {
  reset(
    {
      email: '',
      password: '',
    },
    {
      keepSubmitCount: true,
      keepIsSubmitted: true,
      keepTouched: true,
    }
  );
}, [isSubmitSuccessful]);

フォームに入力を行い”Login”ボタンをクリックします。

reset関数のオプションを設定
reset関数のオプションを設定

設定通り、submitCount, isSubmitted, touchedFields の値がリセットされていないことが確認できます。オプションを利用することでリセットを細かく制御できることがわかりました。

onSubmit 関数の中でも reset 関数のオプションが有効に働くのか確認するためにオプションの設定を行います。先ほどの動作確認の onSubmit 関数の中では submitCount, isSubmitted の値がリセットされなかったので submitCount, isSubmitted の値を false に設定しています。

useEffect の処理が実行されないように忘れずに削除しておきます。


const onSubmit = handleSubmit((data) => {
  console.log(data);
  reset(
    {
      email: '',
      password: '',
    },
    {
      keepSubmitCount: false,
      keepIsSubmitted: false,
      keepTouched: true,
    }
  );
});

フォームに入力を行い”Login”ボタンをクリックします。

onSubmit関数の中でオプション設定
onSubmit関数の中でオプション設定

touchedFields の値は保持できるようになりましたが submitCount, isSubmitted の値は変わらないことがわかります。keepSubmitCount と keepIsSubmitted を true の値にしても結果は同じでした。

reset 関数を利用した場合の formState の値の変化を確認することができました。

setError の設定

フロントエンドのバリデーションエラーは React Form Hook を利用して表示させることがわかりましたがバックエンドのバリデーションエラーはどのようにフロントエンド側で表示させればいいのかと悩んでいる人もいるかと思います。setError を利用することでバックエンドのバリデーションから受け取ったエラーを React Hook Form のエラーに追加するといったことが可能になります。


アプリケーションにユーザ登録を行いたい場合は登録済みかどうかはデータベースにアクセスしなければ確認することができません。その場合はフロントエンドではなくバックエンドのバリデーションが必要となります。

本文書では setError の基本的な設定方法を確認します。

下記のコードではログインボタンを入力後に実行される onSubmit 関数の中でエラーを throw させて setError 関数で email のエラーにエラーを追加しています。


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

type FormData = {
  email: string;
  password: string;
};

function App() {
  const {
    register,
    handleSubmit,
    setError,
    formState: { errors },
  } = useForm<FormData>({
    defaultValues: { email: '', password: '' },
  });

  const onSubmit = handleSubmit((data) => {
    try {
      throw new Error('エラーが発生しています。');
      console.log(data);
    } catch (error: any) {
      setError('email', {
        type: 'server',
        message: error.message,
      });
      console.log(error.message);
    }
  });

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

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

export default App;

フォームに入力を行い”Login”ボタンをクリックすると setError で設定したメッセージが表示されます。

setErrorで設定したメッセージの表示
setErrorで設定したメッセージの表示

setError の引数には存在しないフィールド名を設定すると TypeScript によりエラーが表示されます。

フィールド名のTypeScriptエラー
フィールド名のTypeScriptエラー

メッセージを見ると”email”,“password”以外にも”root.${string}”,“root”も指定できることがわかります。root の後に任意の名前をつけることができるので下記のように設定を行います。


onst onSubmit = handleSubmit((data) => {
  try {
    throw new Error('エラーが発生しています。');
    console.log(data);
  } catch (error: any) {
    setError('root.server', {
      type: 'server',
      message: error.message,
    });
    console.log(error.message);
  }
});

エラーを表示したい場合には下記のように設定を行うことができます。


<div>{errors.root?.server && <p>{errors.root.server.message}</p>}</div>

バックエンド側から戻されるエラーは React Hook Form であつかえるエラーの形に整形することでフォームにエラーを表示させることができます。

入力した値を表示

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

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

getValues による取得

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

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


function App() {
  const {
    register,
    handleSubmit,
    getValues,
    formState: { errors },
  } = useForm<FormData>({
    defaultValues: { email: '', password: '' },
    criteriaMode: 'all',
  });
  //略
<div>{getValues('email')}</div>
isValid, isDirty などの値が入力値によって変化すると再描写により getValues で設定した email の値がブラウザ上に表示されるので formState で利用するのはプロパティの値は errors だけで動作確認しています。

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

watch による取得

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

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


function App() {
  const {
    register,
    handleSubmit,
    getValues,
    watch,
    formState: { errors },
  } = useForm<FormData>({
    defaultValues: { email: '', password: '' },
    criteriaMode: 'all',
  });
  //略
<div>{watch('email')}</div>
<div>{getValues('email')}</div>

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

watch を利用した例

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


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

type formData = {
  showAge: boolean;
  age: string;
};

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

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

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

  return (
    <div className="App">
      <form onSubmit={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 の値を表示したり、条件によって表示させる内容を決めるのに役に立ちます。

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

Zod は React Form Hook 専用のライブラリではなく TypeScript と相性がよく 他のライブラリで TypeScript を利用する場合に頻繁に利用される人気のライブラリです。

Zod のインストール

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


 % npm install @hookform/resolvers zod

Zod の利用方法

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


import * as z from 'zod';

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

定義したスキーマから infer メソッドを利用して型を生成することができます。


type Login = z.infer<typeof loginSchema>;

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


const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm<Login>({
  resolver: zodResolver(loginSchema),
});

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


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

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

type Login = z.infer<typeof loginSchema>;

function App() {
const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm<Login>({
  resolver: zodResolver(loginSchema),
});

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

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={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を利用したバリデーションエラー

refine によるパスワードチェック

refine を利用することでパスワードがマッチするかどうかチェックを行うカスタムバリデーションを行うことができます。refine メソッドの第一引数にバリデーション関数を設定して第 2 引数にはオブジェクトでオプションを設定することができます。


const loginSchema = z
  .object({
    email: z.string().email().min(1),
    password: z.string().min(1),
    confirmPassword: z.string().min(1),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: 'Passwords do not match',
    path: ['confirmPassword'],
  });

上記ではバリデーション関数で data オブジェクトの password プロパティと confirmPassword プロパティの値がチェックされパスしない場合には message の”Passwords do not match”がエラーメッセージに設定されます。エラーがある場合には path に設定した confirmPassword にメッセージが紐づきます。


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

const loginSchema = z
  .object({
    email: z.string().email().min(1),
    password: z.string().min(1),
    confirmPassword: z.string().min(1),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: 'Passwords do not match',
    path: ['confirmPassword'],
  });

type Login = z.infer<typeof loginSchema>;

function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<Login>({
    resolver: zodResolver(loginSchema),
  });

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

  return (
    <div className="App">
      <h1>ログイン</h1>
      <form onSubmit={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>
        <div>
          <label htmlFor="confirmPassword">confirmPassword</label>
          <input
            id="confirmPassword"
            {...register('confirmPassword')}
            type="password"
          />
          <p>{errors.confirmPassword?.message}</p>
        </div>
        <button type="submit">ログイン</button>
      </form>
    </div>
  );
}

export default App;

password と confirmpassword に入力した値が一致しない場合には confirmPassword のエラーとして表示されます。refine メソッドのオプションの path を”password”に変更した場合には入力した値が一致しない場合に password のエラーとして表示されます。

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

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


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

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

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

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

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

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