カスタムフックによるロジックの再利用
Reactには、useState、useContext、useEffectなど、いくつかの組み込みフックが用意されています。時には、より特定の目的のためのフックがあればと思うこともあるでしょう。例えば、データを取得するため、ユーザーがオンラインかどうかを追跡するため、チャットルームに接続するためなどです。これらのフックはReactには見つからないかもしれませんが、アプリケーションのニーズに合わせて独自のフックを作成することができます。
学習内容
- カスタムフックとは何か、そして独自のフックの書き方
- コンポーネント間でのロジックの再利用方法
- カスタムフックの命名と構造化の方法
- カスタムフックを抽出するタイミングと理由
カスタムフック:コンポーネント間でのロジックの共有
ネットワークに大きく依存するアプリ(ほとんどのアプリがそうですが)を開発していると想像してください。ユーザーがアプリを使用している間にネットワーク接続が誤って切断された場合、ユーザーに警告を表示したいと考えています。どのように実装しますか?コンポーネントには2つの要素が必要になるようです:
これにより、コンポーネントはネットワークの状態と同期されます。最初は次のようなコードから始めるかもしれません:
ネットワークのオン・オフを切り替えてみて、このStatusBarがあなたの操作に応じてどのように更新されるかを確認してください。
次に、別のコンポーネントでも同じロジックを使用したいと想像してください。ネットワークがオフの間、「保存」の代わりに「再接続中…」と表示され、無効化される保存ボタンを実装したいと考えています。
まず、isOnline状態とEffectをSaveButtonにコピーして貼り付けることができます:
ネットワークをオフにすると、ボタンの見た目が変わることを確認してください。
これら2つのコンポーネントは正常に動作しますが、ロジックの重複は残念なことです。見た目は異なっていても、ロジックを再利用したいと思っているようです。
コンポーネントから独自のカスタムフックを抽出する
例えば、useStateやuseEffectのように、組み込みのuseOnlineStatusフックがあったと想像してみてください。そうすれば、両方のコンポーネントを簡略化でき、重複を排除できます:
そのような組み込みフックはありませんが、自分で書くことができます。useOnlineStatusという関数を宣言し、以前書いたコンポーネントから重複したコードをすべてその中に移動します:
関数の最後で、isOnlineを返します。これにより、コンポーネントはその値を読み取ることができます:
ネットワークのオン・オフ切り替えで両方のコンポーネントが更新されることを確認してください。
これで、コンポーネント内の重複したロジックが減りました。さらに重要なのは、その内部のコードが、どのように行うか(ブラウザイベントを購読する)ではなく、何をしたいか(オンラインステータスを使う!)を記述していることです。
ロジックをカスタムフックに抽出すると、外部システムやブラウザAPIとのやり取りの複雑な詳細を隠蔽できます。コンポーネントのコードは、実装ではなく意図を表現します。
フック名は常に use
Reactアプリケーションはコンポーネントから構築されます。コンポーネントは、組み込みまたはカスタムのフックから構築されます。他の人が作成したカスタムフックを頻繁に使用することになるでしょうが、時には自分でフックを書くこともあるかもしれません!
以下の命名規則に従う必要があります:
- Reactコンポーネント名は大文字で始める必要があります。例えば、
StatusBarやSaveButtonのように。Reactコンポーネントは、JSXの一部など、Reactが表示方法を知っている何かを返す必要もあります。 - フック名は
useで始まり、その後に大文字が続く必要があります。例えば、useState(組み込み)やuseOnlineStatus(カスタム、このページの前半で紹介したもの)のように。フックは任意の値を返すことができます。
この規則により、コンポーネントを見たときに、その状態、エフェクト、その他のReact機能がどこに「隠れている」可能性があるかを常に把握できます。例えば、コンポーネント内でgetColor()関数呼び出しを見た場合、その名前がuseで始まっていないため、内部にReact状態を含むことはないと確信できます。しかし、useOnlineStatus()のような関数呼び出しは、内部で他のフックを呼び出している可能性が非常に高いです!
注記
リンターがReact用に設定されている場合、この命名規則が適用されます。上のサンドボックスにスクロールして、useOnlineStatusをgetOnlineStatusに名前変更してみてください。リンターが、その内部でuseStateやuseEffectを呼び出すことを許可しなくなることに気づくでしょう。フックやコンポーネントだけが他のフックを呼び出せるのです!
カスタムフックは状態そのものではなく、状態を持つロジックを共有します
先ほどの例では、ネットワークをオン・オフすると、両方のコンポーネントが一緒に更新されました。しかし、単一のisOnline状態変数がそれらの間で共有されていると考えるのは誤りです。このコードを見てください:
これは、重複を抽出する前と同じように動作します:
これらは2つの完全に独立した状態変数とエフェクトです!同じ外部の値(ネットワークがオンかどうか)で同期していたため、たまたま同時に同じ値になったのです。
これをよりよく説明するには、別の例が必要です。このFormコンポーネントを考えてみましょう:
各フォームフィールドには繰り返しのロジックがあります:
- 状態の一部があります(
firstNameとlastName)。 - 変更ハンドラーがあります(
handleFirstNameChangeとhandleLastNameChange)。 - その入力の
value属性とonChange属性を指定するJSXの一部があります。
繰り返しのロジックをこのuseFormInputカスタムフックに抽出できます:
これは1つのvalueという名前の状態変数だけを宣言していることに注意してください。
しかし、FormコンポーネントはuseFormInputを2回呼び出します:
これが、2つの独立したstate変数を宣言するのと同じように機能する理由です!
カスタムフックは、ステートフルなロジックを共有できますが、ステート自体は共有しません。フックの各呼び出しは、同じフックへの他のすべての呼び出しから完全に独立しています。これが、上記の2つのサンドボックスが完全に同等である理由です。必要に応じて、上にスクロールして比較してください。カスタムフックを抽出する前後の動作は同一です。
複数のコンポーネント間でステート自体を共有する必要がある場合は、代わりにステートをリフトアップして渡すようにしてください。
フック間でリアクティブな値を渡す
カスタムフック内のコードは、コンポーネントの再レンダリングごとに再実行されます。そのため、コンポーネントと同様に、カスタムフックも純粋である必要があります。カスタムフックのコードは、コンポーネント本体の一部として考えてください!
カスタムフックはコンポーネントと一緒に再レンダリングされるため、常に最新のpropsとstateを受け取ります。これが何を意味するかを理解するために、このチャットルームの例を考えてみてください。サーバーURLまたはチャットルームを変更してください:
またはroomIdを変更すると、Effectはその変更に「反応」し、再同期します
次に、Effectのコードをカスタムフックに移動します:
これにより、コンポーネントは、内部の仕組みを気にせずにカスタムフックを呼び出すことができます:
これはずっとシンプルに見えます!(ただし、同じことを行います。)
ロジックがpropsとstateの変更に依然として反応することに注意してください。サーバーURLまたは選択されたルームを編集してみてください:
あるフックの戻り値をどのように取得しているかに注目してください:
そしてそれを別のフックへの入力として渡しています:
コンポーネントが再レンダリングされるたびに、最新のChatRoomとroomIdがフックに渡されますserverUrlの出力がuseStateの入力に「供給される」かのようです。)
カスタムフックへのイベントハンドラの受け渡し
より多くのコンポーネントでuseChatRoomを使用し始めると、コンポーネントがその動作をカスタマイズできるようにしたいと思うかもしれません。例えば、現在、メッセージが到着したときの処理ロジックはフック内にハードコードされています:
このロジックをコンポーネントに戻したいとしましょう:
これを機能させるには、カスタムフックが名前付きオプションの一つとしてonReceiveMessageを受け取るように変更します:
これは機能しますが、カスタムフックがイベントハンドラを受け入れる場合、もう一つ改善できる点があります。
コンポーネントが再レンダリングされるたびにチャットが再接続されてしまうため、onReceiveMessageへの依存を追加することは理想的ではありません。このイベントハンドラをエフェクトイベントでラップして依存関係から削除します:
これで、ChatRoomコンポーネントが再レンダリングされるたびにチャットが再接続されることはなくなります。以下は、カスタムフックにイベントハンドラを渡す完全に動作するデモで、試すことができます:
カスタムフックを使用するために、どのようにuseChatRoomが動作するかを知る必要がなくなったことに注目してください。他のコンポーネントに追加したり、他のオプションを渡したりしても、同じように動作します。それがカスタムフックの力です。
カスタムフックを使用するタイミング
重複するコードの小さな断片ごとにカスタムフックを抽出する必要はありません。ある程度の重複は問題ありません。例えば、先ほどのように単一のuseState呼び出しをラップするためにuseFormInputフックを抽出するのはおそらく不要です。
しかし、Effectを書くときは、それをカスタムフックにラップした方が明確になるかどうかを常に検討してください。Effectはあまり頻繁に必要とされるものではありません。そのため、Effectを書いているということは、外部システムと同期したり、Reactに組み込みAPIがない何かを行うために「Reactの外に出る」必要があることを意味します。それをカスタムフックにラップすることで、あなたの意図とデータの流れを正確に伝えることができます。
例えば、2つのドロップダウンを表示するShippingFormコンポーネントを考えてみましょう。1つは都市のリストを表示し、もう1つは選択された都市の地域のリストを表示します。次のようなコードから始めるかもしれません:
このコードはかなり繰り返しが多いですが、これらのEffectを互いに分離したままにしておくのは正しいです。これらは2つの異なるものを同期させるので、1つのEffectに統合すべきではありません。代わりに、上記のShippingFormコンポーネントを、それらの間の共通ロジックを独自のuseDataフックに抽出することで簡略化できます:
これで、ShippingFormコンポーネント内の両方のEffectをuseDataへの呼び出しに置き換えることができます:
カスタムフックを抽出することで、データフローが明確になります。urlを入力として与えると、dataが出力として得られます。EffectをuseData内に「隠す」ことで、ShippingFormコンポーネントを扱う誰かが、そこに不要な依存関係を追加するのを防ぐこともできます。時間が経つと、アプリのEffectのほとんどはカスタムフックの中に収まるでしょう。
カスタムフックはより良いパターンへの移行を助ける
Effectをカスタムフックでラップすることで、これらの解決策が利用可能になったときにコードをアップグレードしやすくなります
この例に戻りましょう:
上記の例では、 はとのペアで実装されています
Reactにはと呼ば
この移行を行うために、コンポーネントを一切変更する必要がなかったことに注目してください:
これが、Effectをカスタムフックでラップすることがしばしば有益であるもう一つの理由です:
- Effectとの間のデータフローが非常に明確になります。
- コンポーネントは、Effectの正確な実装ではなく、意図に集中できるようになります。
- Reactが新機能を追加したとき、コンポーネントを変更することなくそれらのEffectを削除できます。
デザインシステムと同様に、アプリのコンポーネントから一般的な慣用句をカスタムフックに抽出し始めることが役立つかもしれません。これにより、コンポーネントのコードは意図に集中したまま保たれ、生のEffectを頻繁に書くことを避けられます。多くの優れたカスタムフックはReactコミュニティによってメンテナンスされています。
実現方法は一つではありません
ブラウザの一からフェードインアニメーションを実装したいとしますrequestAnimationFrame APIを使用して、refで保持しているDOMノードの不透明度を1になるまで変更できます。コードは次のように始まるかもしれません:
コンポーネントをより読みやすくするために、ロジックをカスタムフックuseFadeInに抽出できます:
このまま useFadeInのコードを維持することもできますが、さらにリファクタリングすることも可能です。例えば、アニメーションループを設定するロジックをuseFadeInから抽出して、カスタムフックuseAnimationLoopにすることができます:
しかし、そのようにする必要はありませんでした。通常の関数と同様に、最終的にはコードの異なる部分の境界をどこに引くかはあなたが決めることです。まったく異なるアプローチを取ることもできます。ロジックをEffect内に保持する代わりに、命令的なロジックの大部分をJavaScriptのクラス内に移動できます:
EffectはReactを外部システムに接続します。Effect間の調整が多ければ多いほど(例えば、複数のアニメーションを連鎖させる場合)、そのロジックをEffectやHookから完全に抽出する(上記のサンドボックスのように)ことが理にかなっています。そうすると、抽出したコードが「外部システム」になります。これにより、EffectはReactの外に移動したシステムにメッセージを送るだけで済むため、シンプルなまま保つことができます。
上記の例では、フェードインのロジックをJavaScriptで記述する必要があると仮定しています。しかし、この特定のフェードインアニメーションは、プレーンなCSSアニメーションで実装する方がよりシンプルで、はるかに効率的です:
場合によっては、Hookすら必要ありません!
まとめ
- カスタムHookを使用すると、コンポーネント間でロジックを共有できます。
- カスタムHookの名前は
useで始まり、大文字が続く必要があります。 - カスタムHookが共有するのはステートフルなロジックであり、ステート自体ではありません。
- あるHookから別のHookにリアクティブな値を渡すことができ、それらは最新の状態を保ちます。
- すべてのHookはコンポーネントが再レンダリングされるたびに再実行されます。
- カスタムHookのコードは、コンポーネントのコードと同様に純粋であるべきです。
- カスタムHookが受け取るイベントハンドラはEffect Eventでラップします。
- カスタムHookを
useMountのように作成しないでください。その目的は具体的に保ちます。 - コードの境界をどのように、どこで選択するかはあなた次第です。
Try out some challenges
Challenge 1 of 5:Extract a useCounter Hook #
This component uses a state variable and an Effect to display a number that increments every second. Extract this logic into a custom Hook called useCounter. Your goal is to make the Counter component implementation look exactly like this:
export default function Counter() {
const count = useCounter();
return <h1>Seconds passed: {count}</h1>;
}You’ll need to write your custom Hook in useCounter.js and import it into the App.js file.
