カスタムフックでロジックを再利用する
カスタムフック:コンポーネント間でのロジック共有
ネットワークに大きく依存するアプリを開発していると想像してください(ほとんどのアプリがそうですが)。アプリの使用中にユーザのネットワーク接続が急に切断された場合に、ユーザに警告を表示したいとします。どのようにすればよいでしょうか? コンポーネントには以下の 2 つが必要になるようです。
- ネットワークがオンラインかどうかを保持する state。
- グローバルの および イベントにリスナを登録し、上記の state を更新するエフェクト。
これにより、コンポーネントはネットワークの状態とするようになります。まずは以下のようなコードができるでしょう。
ネットワークをオン・オフしてみて、この StatusBar
が操作に反応してどのように更新されるか観察してみてください。
さて、別のコンポーネントでも同じロジックを使用したくなったところを想像してください。ネットワークがオフの間は “Save” の代わりに “Reconnecting…” と表示されて無効になるような保存ボタンを実装したいとします。
まず、isOnline
state とエフェクトを、SaveButton
にコピー・ペーストしてみましょう。
ネットワークをオフにするとボタンの外観が変わることを確認してください。
これらの 2 つのコンポーネントはうまく動作していますが、それらの間でロジックが重複しているのは残念な感じがします。視覚的な外観は異なるにせよ、ロジックはそれらの間で再利用したいと思うことでしょう。
コンポーネントから独自のカスタムフックを抽出する
や と同様に、組み込みの useOnlineStatus
というフックがあるところを、ちょっと想像してみてください。それがあれば、これらのコンポーネントを簡略化し、両者で重複しているコードを取り除けるでしょう。
このような組み込みのフックは存在しませんが、自分で書くことは可能です。useOnlineStatus
という関数を宣言して、先ほど作成したコンポーネントから、重複しているコードをすべて移動しましょう。
関数の最後で isOnline
を返します。これにより、コンポーネント側でその値を読み取ることができるようになります。
ネットワークのオン・オフを切り替えることで、両方のコンポーネントが更新されることを確認してください。
これで、コンポーネント間のロジックの重複が減りました。さらに重要なのは、コンポーネント内のコードが、「オンラインステータスを使用 (use) する」という、何をしたいのかの記述になっているということです。どのようにして実現するのか(ブラウザのイベントに登録する)ではありません。
ロジックをカスタムフックに抽出することで、外部システムやブラウザ API とのやり取りに関する面倒な詳細を隠蔽することができます。あなたのコンポーネントのコードは、実装方法ではなく意図を表現するようになるのです。
フックの名前は常に use
で始める
React アプリケーションはコンポーネントから構築されます。コンポーネントは、組み込みのものやカスタムのものなど、フックから構築されます。他の人が作成したカスタムフックをよく使うことになりますが、時には自分で書くこともあるでしょう!
以下の命名規則に従う必要があります。
- React コンポーネントの名前は大文字で始まる必要があります。例えば、
StatusBar
やSaveButton
などです。React コンポーネントは、JSX のような、React が表示方法を知っているものを返す必要もあります。 - フックの名前は
use
で始めて大文字を続ける必要があります。例えば、(組み込みのもの)やuseOnlineStatus
(上述のようなカスタムのもの)などです。フックは任意の値を返すことができます。
この慣習により、コンポーネントを見るだけで、その中の state、エフェクト、その他の React 機能がどこに「隠れている」可能性があるか、常に把握できることが保証されます。例えば、コンポーネント内で getColor()
関数の呼び出しを見た場合、名前が use
で始まっていないので React の state が内部に含まれている可能性はありません。しかし useOnlineStatus()
のような関数呼び出しは、内部で他のフックを呼び出している可能性が高いです!
さらに深く知る
レンダー中に呼び出されるすべての関数を use プレフィックスで始めるべきか?
レンダー中に呼び出されるすべての関数を use プレフィックスで始めるべきか?
いいえ。フックを呼び出さない関数は、フックである必要はありません。
関数がフックを呼び出さない場合は、use
プレフィックスを避けてください。代わりに、use
プレフィックスなしの通常の関数として記述してください。例えば、以下の useSorted
はフックを呼び出さないので、代わりに getSorted
という名前にしましょう。
これにより、コードはこの通常の関数を、条件分岐内を含むどんな場所からでも呼び出すことができます。
関数の内部で 1 つ以上のフックを使用している場合は、use
プレフィックスを付ける(つまりフックにする)必要があります。
厳密には、これは React によって強制されているわけではありません。原理上は、他のフックを呼び出さないフックを作成することは可能です。混乱を招き余計な制限が加わるため、このようなパターンは避けるのが賢明です。ただし、まれにこれが役立つ場合もあります。例えば、関数が現在はフックを使用していない場合でも、将来的にフック呼び出しを追加する予定があるかもしれません。その場合、use
プレフィックスを使って名前を付けておくことは理にかなっているでしょう。
こうすれば、コンポーネントはこのコードを条件分岐内で呼び出すことができなくなります。中でフック呼び出しを実際に追加したときに、このことが重要になります。現在も将来も内部でフックを使用する予定がない場合は、フックにしないでください。
カスタムフックは state 自体ではなく、state を使うロジックを共有する
前の例では、ネットワークをオンまたはオフに切り替えると、両方のコンポーネントが同時に更新されました。しかし、isOnline
という単一の state 変数がそれらの間で共有されていると考えるのは間違いです。こちらのコードを見てください。
これは、重複を抽出する前と同じ方法で動作します。
これらは、完全に独立した state 変数とエフェクトです! 同時に同じ値になっているのは、たまたま(ネットワークがオンかどうかという)同一の外部の値と同期させたからです。
より分かりやすく説明するために、別の例を考えてみましょう。この Form
コンポーネントを考えてみてください。
各フォームフィールドに対応して、繰り返しのロジックがあります。
- state 変数(
firstName
とlastName
)。 - change ハンドラ(
handleFirstNameChange
とhandleLastNameChange
)。 - 対応する入力フィールドに
value
とonChange
属性を指定するための JSX。
この繰り返しのロジックを useFormInput
というカスタムフックに抽出することができます。
コード内で宣言されているのは value
という 1 つの state 変数だけであることに注目してください。
しかし Form
コンポーネントは useFormInput
を 2 回 呼び出しています。
これが、2 つの別々の state 変数を宣言するのと同じように動作する理由です!
カスタムフックは、state 自体ではなく、state を扱うロジックを共有できるようにするためのものです。フックの呼び出しは、同じフックの他の場所からの呼び出しとは完全に独立しています。これが、上記の 2 つのサンドボックスが完全に同等である理由です。よろしければ、スクロールして上に戻って見比べてみてください。カスタムフックを抽出する前と後で、挙動は全く同一です。
複数のコンポーネント間で state 自体を共有する必要がある場合は、ようにしてください。
フック間でリアクティブな値を渡す
カスタムフック内のコードは、コンポーネントの再レンダーごとに実行されます。そのため、コンポーネントと同様に、カスタムフックは。カスタムフックのコードは、コンポーネントの本体の一部だと考えてください!
カスタムフックはコンポーネントと一緒に再レンダーされるため、常に最新の props と state を受け取ります。どういうことか理解するために、以下のチャットルームの例を考えてみましょう。サーバの URL やチャットルームを変更してみてください。
serverUrl
や roomId
を変更すると、エフェクトは再同期されます。コンソールのメッセージを見ると、エフェクトの依存配列に変更があるたびにチャットが再接続されていることがわかります。
次に、エフェクトのコードをカスタムフックに移動します。
これにより、ChatRoom
コンポーネントは単にカスタムフックを呼び出せばよく、内部でどのように動作するかを気にしなくてもよくなります。
これはずっとシンプルに見えます!(しかしやっていることは同じです。)
依然として props や state の変更に対しロジックが反応していることに注意してください。サーバ URL やルームを編集してみてください。
ここでは、あるフックの返り値を取得して…
…それを別のフックに入力として渡しています。
ChatRoom
コンポーネントが再レンダーされるたびに、あなたのフックには最新の roomId
と serverUrl
が渡されます。したがって、再レンダー後にこれらの値が異なる場合にはエフェクトがチャットの再接続を行います。(オーディオやビデオ処理ソフトウェアを使ったことがある場合、このようなフックのチェーンは、視覚エフェクトやオーディオエフェクトのチェーンに似ていると感じるかもしれません。useState
の出力が useChatRoom
の入力に “フィードイン” しています。)
カスタムフックにイベントハンドラを渡す
useChatRoom
がより多くのコンポーネントで使用されるようになると、コンポーネント側でその動作をカスタマイズしたくなってくるでしょう。例えば現在のところ、メッセージが届いたときの処理ロジックはフック内にハードコードされています。
このロジックをコンポーネント側に戻したいとしましょう。
これを実現するために、カスタムフックを変更して、onReceiveMessage
を名前付きオプションの 1 つとして受け取るようにします。
これで動作しますが、カスタムフックがイベントハンドラを受け取る場合、改善できることがもう 1 つあります。
onReceiveMessage
を依存値として追加すると、コンポーネントが再レンダーされるたびにチャットが再接続されてしまうため、あまり望ましくありません。。
これで、ChatRoom
コンポーネントが再レンダーされるたびにチャットが再接続されることはなくなります。以下が、イベントハンドラをカスタムフックに渡す完全なデモです。試してみてください。
useChatRoom
を使うために内部の動作を知らなくても良くなったことに着目してください。他のコンポーネントに追加したり、他のオプションを渡したりしても、同じように動作します。これがカスタムフックの威力です。
カスタムフックを使うタイミング
あらゆる小さなコードの重複に対してカスタムフックを抽出する必要はありません。多少の重複は問題ありません。例えば、先ほどのように 1 回の useState
呼び出しをラップするだけの useFormInput
フックを抽出することは、おそらく不要でしょう。
ただし、エフェクトを書くときは常に、更にそのエフェクトをカスタムフックにラップすることでより分かりやすくならないか、検討するようにしてください。。エフェクトを書くということは、外部システムと同期するために「React の外に踏み出す」必要がある、もしくは React に組み込みの API がない何かを行う必要があるということです。カスタムフックにラップすることで、あなたの意図とデータの流れを正確に表現することができます。
例えば、都市のリストを表示するドロップダウンと、そこで選択中の都市内にある地区のリストを表示する別のドロップダウンがある、ShippingForm
というコンポーネントを考えてみましょう。まずは次のようなコードを書くことになるでしょう。
このコードはかなりの繰り返しになっていますが、。これらは 2 つの異なるものを同期しているので、1 つのエフェクトに統合すべきではありません。代わりに、これらに共通のロジックを独自の useData
フックとして抽出することで、上記の ShippingForm
コンポーネントを簡略化することができます。
これで、ShippingForm
コンポーネントの両方のエフェクトを useData
の呼び出しに置き換えることができます。
カスタムフックに抽出することで、データの流れが明示的になります。url
を入力し data
を出力しているということです。useData
の中にエフェクトを「隠す」ことで、ShippingForm
コンポーネントで作業中の誰かがを追加してしまうことを防げます。時間が経つにつれて、アプリのほとんどのエフェクトはカスタムフックに書かれるようになるでしょう。
さらに深く知る
カスタムフックは具体的かつ高レベルなユースケースに対して使う
カスタムフックは具体的かつ高レベルなユースケースに対して使う
まず、カスタムフックの名前を選ぶところから始めましょう。明確な名前を選ぶことが難しいと感じる場合、エフェクトがコンポーネントの他のロジックとあまりにも密接に関連しており、まだ抽出する準備ができていないということかもしれません。
理想的には、カスタムフックの名前は、コードをあまり書かない人でも、何をするのか、何を受け取るのか、何を返すのかを推測できるほどに明確であるべきです。
- ✅
useData(url)
- ✅
useImpressionLog(eventName, extraData)
- ✅
useChatRoom(options)
外部システムと同期する場合、カスタムフックの名前は、そのシステム固有の専門用語を使用したより技術的なものになるかもしれません。そのシステムに精通している人にとって明確である限り、問題はありません。
- ✅
useMediaQuery(query)
- ✅
useSocket(url)
- ✅
useIntersectionObserver(ref, options)
カスタムフックは具体的かつ高レベルのユースケースに対して使うようにしてください。useEffect
API 自体の代替物ないし便利なラッパとして機能させるための、カスタム「ライフサイクル」フックを作ったり使ったりしないようにしてください。
- 🔴
useMount(fn)
- 🔴
useEffectOnce(fn)
- 🔴
useUpdateEffect(fn)
例えば、この useMount
フックは、あるコードが「マウント時」にのみ実行されるようにしようとしています。
useMount
のようなカスタム「ライフサイクル」フックは、React のパラダイムと適合しません。例えば、このコードサンプルには間違いがあります(roomId
や serverUrl
の変更に「反応」しません)が、リンタは useEffect
の直接的な呼び出しのみをチェックするため、これに対して警告を出してくれません。あなたのフックのことは知らないからです。
エフェクトを書く場合は、まず React の API を直接使ってください。
その後、様々な高レベルのユースケースに対してカスタムフックを抽出するようにします(必須ではありません)。
良いカスタムフックとは、動作を制約することで呼び出し側のコードをより宣言的にするものです。例えば、useChatRoom(options)
はチャットルームへの接続のみを行い、useImpressionLog(eventName, extraData)
はアナリティクスに表示ログを送信することのみを行います。あなたのカスタムフックの API がユースケースを制約しない非常に抽象的なものである場合、長期的には解決される問題よりも多くの問題を引き起こす可能性が高いでしょう。
カスタムフックはより良いパターンへの移行を支援する
エフェクトは です。「React の外に踏み出す」必要があり、当該ユースケースに対してより良い組み込みのソリューションがない場合に使用するものです。長期的な React チームの目標は、より具体的な問題に対してより具体的なソリューションを提供することで、アプリ内のエフェクトの数を最小限に減らすことです。エフェクトをカスタムフックにラップしておくことで、これらのソリューションが利用可能になったときにコードのアップグレードが容易になります。
こちらの例に戻りましょう。
上記の例では、useOnlineStatus
は と のペアで実装されています。しかし、これは最善のソリューションではありません。考慮されていないエッジケースがいくつかあります。例えば、コンポーネントがマウントされたとき isOnline
は true
であると仮定していますが、ネットワークがすでにオフラインになっていた場合、これは誤りです。ブラウザの API を使ってそれをチェックすることはできますが、それを直接使うと、サーバで初期 HTML を生成する際には動作しません。要するに、このコードには改善の余地があるということです。
React には、これらの問題をすべて解決してくれる専用の API である が含まれています。以下は、この新しい API を活用して書き直された useOnlineStatus
フックです。
どのコンポーネントも変更することなしにこの移行ができたことに注目してください。
これが、カスタムフックにエフェクトをラップすることが有益であるもうひとつの理由です。
- エフェクトに出入りするデータの流れが非常に明確になる。
- コンポーネントがエフェクトの実装そのものではなく、意図にフォーカスできるようになる。
- React が新しい機能を追加したときに、コンポーネントを変更せずにエフェクトを削除できるようになる。
と同様に、アプリのコンポーネントから共通の定型コードをカスタムフックに抽出することは役立つでしょう。これにより、コンポーネントのコードは意図を表現するようになり、生のエフェクトを頻繁に書くことを避けることができるようになります。React コミュニティでは多くの優れたカスタムフックがメンテナンスされています。
さらに深く知る
将来 React はデータフェッチのための組み込みソリューションを提供するか?
将来 React はデータフェッチのための組み込みソリューションを提供するか?
まだ詳細は検討中ですが、将来的にはデータフェッチを以下のように書くことになるでしょう。
アプリで上記のような useData
のようなカスタムフックを使用しておくことで、最終的に推奨されるアプローチに移行する際に、コンポーネントごとに手動で生のエフェクトを書く場合よりも変更が少なくて済みます。ただし、古いアプローチでも問題なく動作するので、生のエフェクトを書くことに満足している場合は、それを続けることもできます。
やり方は 1 つではない
ブラウザの API を使って、ゼロからフェードインアニメーションを実装したいとしましょう。アニメーション用のループを設定するエフェクトから始めることになるでしょう。アニメーションの各フレームでは、 DOM ノードの不透明度を 1
になるまで更新していきます。最初のコードは次のようになるでしょう。
このコンポーネントをより読みやすくするために、useFadeIn
カスタムフックにロジックを抽出することができます。
この useFadeIn
はこのままでも構いませんが、さらにリファクタリングすることも可能です。例えば、アニメーションループの設定ロジックを useFadeIn
の外のカスタム useAnimationLoop
フックへと抽出することができます。
ただし、これは必須ではありませんでした。通常の関数と同様、コードのどこに分割線を引いていくのかは、最終的にあなたが決めることです。また、まったく異なるアプローチを取ることもできます。エフェクト内にロジックを保持する代わりに、命令型のロジックのほとんどを JavaScript の内に移動することもできるでしょう。
エフェクトとは React を外部システムに接続することができるものです。エフェクト間で多くの調整が必要になればなるほど(例えば、複数のアニメーションを連動させるなど)、上記のサンドボックスのようにエフェクトやフックからロジックを完全に抽出してしまうことがより意味を持つようになります。そうすればその抽出したコードこそが「外部システム」となります。これにより、その React 外に移動したシステムにメッセージを送るだけでよくなるため、エフェクトはシンプルに保たれるでしょう。
なお上記の例では、フェードインのロジックを JavaScript で記述する必要があると仮定していました。ただしこの特定のケースに関して言えば、このフェードインアニメーションは単純な で実装する方がずっと簡単で効率的です。
ときには、そもそもフック自体が不要ということです!
まとめ
- カスタムフックを使ってコンポーネント間でロジックを共有できる。
- カスタムフックの名前は
use
で始めて大文字を続ける必要がある。 - カスタムフックは state 自体ではなく、state を使うロジックを共有する。
- あるフックから別のフックにリアクティブな値を渡すことができ、それらは最新の状態に保たれる。
- すべてのフックはコンポーネントが再レンダーされるたびに再実行される。
- カスタムフックのコードは、コンポーネントコードと同様に純粋である必要がある。
- カスタムフックが受け取るイベントハンドラはエフェクトイベントにラップする。
useMount
のようなカスタムフックを作成してはいけない。常に目的は具体的なものにする。- コードの境界をどこにどのように置くかはあなたが決定する。