Reactには、コンポーネント内でデータを保持するための2つの方法があります。それがuseStateuseRefです。一見すると、これらは似ているように見えます。どちらも読み取り可能な値を返し、どちらも再レンダリングをまたいで生存し、どちらもクリックのたびに値を保持することができます。しかし、選択を誤ると、画面が更新されないか、あるいは不要なレンダリングが延々と続くことになります。この選択は構文の問題ではありません。「Reactがその変化を知る必要があるかどうか」の問題なのです。

各フックが実際に行っていること

useStateは、Reactがコンポーネントと通信するための公式な手段です。これを呼び出すと、値とセッター関数が返されます。Reactはその値をコンポーネントのアイデンティティの一部として追跡します。セッターが実行されると、Reactは「何かが変わった」と判断し、画面が最新の状態に追いつけるよう、新しいレンダリングをスケジュールします。

一方、useRefは、currentプロパティを持つ単なるJavaScriptオブジェクトに過ぎません。Reactは、すべてのレンダリングにおいて、全く同じオブジェクトの参照を渡すことを保証します。Reactは、その中身を監視しません。someRef.currentを書き換えても、それは静かに行われます。Reactは反応しません。

その「静かさ」こそが、このフックの肝です。Refは状態(state)の代わりではなく、エスケープハッチ(脱出策)なのです。

レンダリングの境界線

状態(state)を変更すると、コンポーネントは再レンダリングされます。これは初心者が最も期待する挙動であり、新しいデータが画面に表示される必要がある場合には、まさにこれが求められる動作です。カウンター、フォームの入力フィールド、取得したユーザーリストなど、ユーザーが目にするものは、おそらくstateに属すべきものです。Reactのデータフロー全体は、「stateの変化がレンダラーに通知され、DOMを同期させる」という考え方に基づいて構築されています。

Refを変更しても、視覚的な変化は何も起こりません。変数は即座に、かつ同期的に更新されますが、コンポーネントは再レンダリングされません。この特性により、Refは、視覚的な出力の一部にはならないものの、コンポーネントの内部動作を支える値に最適です。タイマーのID、以前のpropsのスナップショット、あるいはDOMへの直接的なハンドルなどを思い浮かべてください。カルーセルの自動再生を制御するインターバルIDについて、UIは関心がありません。UIが関心があるのは、どのスライドが表示されているかだけです。したがって、インターバルIDはrefに属すべきです。

Stateが適切なツールとなる場合

値がUIの表面(見た目)の一部である場合は、いつでもuseStateを使用してください。

入力フィールドは分かりやすい例です。ユーザーがメールアドレスを入力し、それを検証してボックスの下にエラーメッセージを表示する必要がある場合、そのメールアドレスの文字列はstateである必要があります。バリデーションのロジックとエラーバナーの両方が最新の値に依存しており、Reactがバナーを更新できるのは、stateが再レンダリングをトリガーしたからです。

カウントやトグルも定番です。スコアを増やすボタン、モーダルの開閉フラグ、タブのインデックスなど、これらはすべて、値の変化に合わせてレンダリング結果が変わるため、stateを通じて扱われます。検索文字列に依存するフィルタリングされたリストのような派生値であっても、通常はソースとなる値がユーザーに見えるため、stateから始まります。

また、理解しておくべきタイミングに関する細かな違いがあります。Stateの更新は非同期であり、バッチ処理されます。1つのイベントハンドラー内でsetCount(count + 1)を3回呼び出しても、Reactは3回レンダリングしません。それらを1つの更新にまとめて処理します。また、実行中の関数内の変数countも、次のレンダリングまで古いままです。このバッチ処理は機能の一つであり、アプリケーションの高速化に寄与しています。しかし、これは、state変数がすぐ次の行で新しい値を反映しているとは期待できないことを意味します。

Refが救世主となる場合

useRefは、見た目のためではなく、内部的な仕組みのために...