React Hook Formの基本の使い方や具体的な活用例については、以前の記事でまとめました。
今回は、React Hook Formがフォームの管理をしている仕組みを理解したいと思います。
React Hook Formのメリット
仕組みを理解する前に、特徴や強みを知っておきたいと思います。
公式ドキュメントでは、他のReactのフォームライブラリであるFormikやredux formと比較して、以下のようなメリットが挙げられています。
- 非制御コンポーネント(Uncontrolled Components)を採用しており、input要素をフォーム全体の再レンダリングなしに監視して変更できるので、レンダリング回数を減らすことが出来る
- state管理などのコード記述量を減らすことが出来る
- パッケージが軽量
特徴
上記のようなメリットを可能にしている、React Hook Form最大の特徴は、状態管理をstateとpropsではなくDOMで行う**非制御コンポーネント(Uncontrolled Components)**でフォームの値を扱うことです。
useFormというカスタムフックが提供するAPIであるregister関数を使用して、**参照(ref属性)**をuseFormに登録することで、フォームの状態をコントロールしています。
ref…? 非制御コンポーネント…?
どうやらReact Hook Formの技術のもとになっているのは、refと非制御コンポーネントというもののようです。
Refとは
Reactの公式ドキュメントでは、以下のように書かれています。
Refは、Renderメソッドで作成されたDOMノードやReact要素にアクセスする方法を提供します。
基本的にReactでは、stateが状態を管理・propsがコンポーネント間でデータの受け渡しをする唯一の方法で、子要素を変更するには新しいpropsで再レンダリングをする必要がある。
それに対してRefは、レンダー毎に値が作られるのではないので、レンダーと値が対応しておらず、同じレンダーの中で値を更新することもできる。
ReactにおけるRefには、
- Refオブジェクト
- ref属性
の2つの概念がある。
-
Refオブジェクト
-
コンポーネントがマウントされた時からアンマウントされるまで存在し続ける。(通常Reactで const obj = { } と記述すると、レンダリング毎に値は同じだが新しいオブジェクトが生成される。)
-
currentというプロパティを持っていおり、ここで値を保持する。
-
Refオブジェクトでは、currentの値を更新してもReactにそのことが通知されることはない。
-
RefオブジェクトはReact.createRef()を使用して作成され、ref属性を用いてReact要素に紐づけられる。
-
-
ref属性
- HTML要素やクラスコンポーネントにはref属性を設定することができ、この属性にはRefオブジェクトを渡すことができる。
- 先述のようにRefオブジェクトの値の変更がReactに通知されることは無いので、ref属性を設定している要素の状態が変わったタイミングでイベントを発火させることもできない。そのようなことを行いたい場合は、ref属性に関数を渡す必要がある。
このようにRefを使った操作は、Reactのstateとpropsによって制御するという基本的な考え方からは外れており、Reactの公式ドキュメントでも多用することは推奨されていません。
制御コンポーネントと非制御コンポーネントとは
制御コンポーネント
フォームの状態をReactのstateで管理する、通常のReactの書き方です。
例えば以下のような感じ。
const ControlledForm = () => {
const [name, setName] = useState('');
const handleChange = event => setName(event.target.value);
const handleSubmit = () => console.log(name);
return (
<div>
<input type="text" value={name} onChange={handleChange} */>*
<button onClick={handleSubmit} >Submit</button>
</div>
);
}
- 入力値を管理するstateを作成
- フォームのvalueと作成したstateを同期
- ユーザーが入力を変更する度に、stateをユーザーの入力値に更新する
- 必要なタイミング(送信ボタンが押された時など)でstateの値を取得する
というような流れで、stateを使ってフォームの状態を管理します。
(onChangeイベントで値を監視するので)Inputの値は常にstateと同期されます。
言い換えると、入力値が変わるたびに再レンダリングが起きているということです。
非制御コンポーネント
stateの更新に対してイベントハンドラを書くのではなく、Refを使用してDOMからフォームの値を取得します。
本来stateで持つデータを、Refを使ってDOM自身で扱います。
const UncontrolledForm = () => {
const formRef = React.useRef() // Refオブジェクトの生成
const handleSubmit = event => {
event.preventDefault();
console.log(formRef.current.value);
}
return (
<>
<div>
<input ref={formRef} type="text" name="name" /> // ref属性に登録
<button onClick={handleSubmit}>Submit</button>
</div>
</>
);
}
- React.createRef()を用いてDOMを監視するためのRefオブジェクトを作成
- inputタグのref属性に、作成したRefオブジェクトを登録
- onClickイベントをトリガーにして入力値を取得
というような流れで、inputの値をRefオブジェクトを通じてDOMから取り出すことで、フォームの状態を管理します。
非制御コンポーネントのメリット・デメリット
- メリット
- onChangeイベントごとに再レンダリングする制御コンポーネントに比べ、必要なタイミングで値を取得するので軽量
- stateに関する記述をなくせる
- HTML・JavaScriptのネイティブの実装に近いので、他のフレームワーク等へ移植しやすい
- デメリット
- 必要なタイミングでDOMから値を取り出すので、入力ごとにvalidationする・入力に対応して表示を変えるなどの処理が難しい。(React Hook Formではオプションにより可能)
改めて、React Hook Formの仕組みとは
非制御コンポーネントでは、通常は入力毎の値を追うことは出来ませんが、React Hook Formでは入力毎にバリデーションを実行することもできます。
どうやって実現しているのかというと、、、
まずReact Hook Formでは、React Hook FormのAPIであるregisterを使って、フォームの要素(inputやselectなど)のref属性に登録しています。
以下のような書き方の部分です。
<input
{...register("name", {
required: "名前を入力してください"
})}
type="text"
/>
そしてregisterからどうやって変更を検知しているのか。
しっかり調べてくださっている方がいました。
以下の方の記事を拝借すると、最終的にregisterは受け取ったrefのaddEventListenerメソッドを呼び出し、blur, change, inputのイベントリスナーを登録しているとのこと。
React Hook Formは非制御コンポーネントからどうやって変更を検知しているのか - commmune Engineer Blog
これで、入力値を監視したり変更を検知できているというわけですね。
// React Hook Form のコードから抜粋
ref.addEventListener(
shouldAttachChangeEvent ? EVENT.CHANGE : EVENT.INPUT,
handleChange,
);
ref.addEventListener(EVENTS.BLUR, handleChange);
export const EVENTS = {
BLUR: 'blur',
CHANGE: 'change',
INPUT: 'input',
}
参考文献