React Hook Formの基本を理解してフォームを作成してみよう

React Hook Formはフォームバリデーションライブラリです。input要素に入力した値を取得するだけではなくバリデーション機能なども備えており簡単にフォームを実装することができます。入力フォームの作成が嫌いな人もライブラリの力を借りることでフォーム作成の手間を軽減することができます。
本文書ではすぐに実践で活用できるようにバリデーションやエラーメッセージの表示などフォームを作成する上で必要な機能を中心にシンプルなコードを利用して説明しています。
利用するReact Hook Formライブラリのバージョンは7です。Reactのバージョンは18.2.0です。
目次
ライブラリを利用しない場合
スキップしても構いませんがReact Hook Formを使ってフォームを作成する前にライブラリを使用しない場合のReactでのフォームの作成方法について確認しておきます。一般的には2つの方法があり、一つはuseState Hookを利用する方法、もう一つはuseRefを利用する方法です。前者はControlled Component, 後者はUncontrolled Componentと呼ばれます。Reactによってinput要素のvalueを制御するかどうかの違いがあります。
emailとpasswordの2つの入力欄を持つログインフォームを作成して動作確認を行います。
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)によって制御されています。
ブラウザで確認すると以下のログイン画面が表示されます。

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} />
</div>
<div>
<button type="submit">ログイン</button>
</div>
</form>
</div>
);
}
export default App;
useRefで作成したログイン画面とuseStateで作成したログイン画面に違いがなくどちらも入力後にログインボタンを押すと入力した値がオブジェクトで表示されます。
画面や出力される内容に違いはありませんがuseState, useRefという異なる機能を用いています。異なる機能を利用しているため大きな違いがあり、useStateでは文字を入力する度に”再描写(Re-render)”が行われますがuseRefの場合には”再描写(Re-render)”が行われません。再描写によりパフォーマンスに影響があることがわかります。
useRefについては下記の文書でも説明を行っています。
React Hook Formの設定
Reactでの入力フォームの作成方法の復習ができたのでここからはReact Hook Formを利用して設定を行っていきます。
ライブラリのインストール
React Hook Formライブラリのインストールはnpmコマンドで行います。
% npm install react-hook-form
React Hook Formを利用した場合
useStateとuseRefで作成したログインフォームをReact Hook Formを利用して書き換えます。
React Hook Formという名前の通りカスタム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;
ログイン画面は先ほどまでと同じで入力フォームも入力後ログインボタンをクリックして表示されるオブジェクトも同じです。
書き換えを行うことができたので中身を確認していきます。input要素の…register関数が気になると思うのでregister関数を確認していきましょう。register関数についてはReact Hook Formのドキュメントを確認することでどのようなものから構成されているのか理解することがきます。
ドキュメントからregister関数に引数を指定することでname(属性), ref, onBlur, onChangeが戻されることがわかります。

最初は不自然に感じた下記のinput要素もregister関数の中身がわかればシンプルであることがわかります。
<input id="email" {...register('email')} />
register関数に引数を指定することで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属性に設定されます。

registerの関数によって戻される値がわかったので以後はコード量も少ない以下の形を利用していきます。
<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が実行できないことがわかりましたがブラウザ上にはエラーメッセージが表示されないため何が行っているのかがわかりません。次はバリデーションに失敗した場合のエラーの表示方法を確認します。
エラーの表示
バリデーションのエラーを表示するためには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が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.pattern && (
<div>{errors.password.types.pattern}</div>
)}
{errors.password?.types.minLength && (
<div>8文字以上入力してください。</div>
)}
ブラウザ上でPasswordの入力欄に数字を入れて”ログイン”ボタンをクリックすると失敗したバリデーションエラーの情報が表示されます。

初期値の設定
useForm Hookの引数を利用して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の値はtrueになっていますがフォーム中のいずれかの入力欄にフォーカスを当てて外すか文字を1文字でも入力するとisDirtyの値はtureからfalseにかわります。
例えばページを開いた直後、ログインのbutton要素のdisable属性にisDirtyを設定することでボタンをクリックすることができせん(無効)がEmailまたはPasswordの入力欄にフォーカスを当てて外すか文字を入力するとisDirtyがtureから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の文字を入力する必要があります。

デフォルト値の設定
useFormでデフォルト値を設定することで入力欄の値がデフォルト値の場合はフォーカスを当てて外してもisDirtyがfalseになることはなくなります。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入力欄の影響は受けません。
<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;
しかしデフォルトではログインボタンをクリックするまではバリデーションは実行されないのでログインボタンが有効になることはありません。この場合はmodeをデフォルトのonSubmitからonChangeやonBlurに変更する必要があります。
function App() {
const {
register,
handleSubmit,
formState: { isValid, errors },
} = useForm({
defaultValues: { email: '', password: '' },
mode: 'onChange',
criteriaMode: 'all',
});
//略
modeをonChangeに変更すると文字を入力する度にバリデーションが実行されるのでバリデーションにパスするとログインボタンをクリックできるようになります。
入力した値を表示
入力した値を表示するための方法がいくつか準備されていますがここでは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;

チェックボックスにチェックをすると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の値を表示したり、条件によって表示させる内容を決めるのに役に足します。
本文書で説明した以外にもuseFormのオプションやuseFormから戻されるオブジェクトのプロパティはありますがReact Hook Formを利用する上で必要な基礎は理解できたかと思います。ぜひReact Hook Formを利用してフォームを作成してみてください。