カスタムフックは、名前が use で始まり、ほかのフックを呼ぶ関数です。2つのコンポーネントが同じstateを持つロジックを必要とするときに書きます。useState や useEffect の呼び出しを関数に移し、コンポーネントが必要とするものを返し、各コンポーネントからそれを呼びます。
どちらのネタバレも useToggle を呼んでいますが、一方を開いてももう一方は閉じたままです。呼び出しごとに自分の useState が実行されるので、各コンポーネントは自分の値を持ちます。カスタムフックが共有するのはロジックであって、stateではありません。
書くときのルール
- 名前は
useと大文字で始める:useToggle、useOnlineStatus。フックのlintルールは、この接頭辞でフックの呼び出しを見つけて確認します。 - 少なくとも1つのフックを呼ぶ。フックを呼ばない補助関数は普通の関数です。条件分岐の中を含めどこからでも呼べるように、
useFormatDateではなくformatDateと名付けてください。 - フックのルールに従う:中ではトップレベルでフックを呼び、それ自体はコンポーネントかほかのカスタムフックからだけ呼びます。
- 役に立つものを何でも返す。配列(
[on, toggle])は呼び出す側が値の名前を変えるときに便利で、オブジェクト({ data, loading, error })は3つ以上のときに読みやすくなります。
useLocalStorage:再読み込みしても残るstate
useState は再読み込みするとすべてを忘れます。このフックは初期値を localStorage から読み、変更のたびに書き戻します。
プレビューはブラウザが localStorage をブロックするサンドボックスのフレームで動いているので、ここではコンソールに警告が表示され、フックは普通のstateにフォールバックします。普通のページなら、名前は再読み込みしても残ります。try/catch はこのプレビューのためだけではありません。プライベートブラウジング、ブロックされたCookie、容量の上限によって、実際のアプリでもストレージは例外を投げます。useState に関数を渡しているので、ストレージを読むのは最初のレンダリングの1回だけです。
useDebounce:ユーザーが入力し終わるまで待つ
検索欄は、キーを押すたびにリクエストを送るべきではありません。useDebounce は、値が決まった時間変わらなくなってから更新される、その値のコピーを返します。
単語をすばやく入力してください。Typed はすべてのキーに追従し、Debounced は入力を止めてから0.5秒後に追いつき、コンソールには検索が1回だけ記録されます。キーを押すたびに value が変わるので、Reactは新しいタイマーを始める前にクリーンアップ(clearTimeout)を実行します。500 を 0 に変えると、キーを押すたびに検索が記録されます。
useWindowWidth:クリーンアップ付きで購読する
ブラウザを監視するフックは、コンポーネントが消えるときにリスナーを削除しなければなりません。そうしないと、マウントのたびにリスナーが1つずつ残ります。
ブラウザのウィンドウの大きさを変えると、数値が追従します。ボタンをクリックすると、リスナーが削除されたことがコンソールに表示されます。エフェクトから return を削除し、何回かマウントとアンマウントを繰り返してみてください。毎回 added の行が出て removed は出ないので、古いリスナーが積み重なっていきます。このようなブラウザの値には、Reactがまさにそのために作ったuseSyncExternalStoreがあり、カスタムフックの中で呼ぶのに適しています。
共有されるのはロジックで、stateではない
同じフックを呼ぶ2つのコンポーネントは、その中のすべてを別々に2つ持ちます。フックがグローバル変数のように動くと思っている人は、これに驚きます。
どちらかのボタンをクリックすると、そのカウンターだけが動きます。両方に同じ数値を表示すべきなら、stateを App に移して下へ渡すか、コンテキストに入れてください。カスタムフックは、便利なように共有stateを包むことができます(CartContext を読む useCart())が、共有を実現しているのはフックではなくコンテキストです。
カスタムフックを書くべきとき
同じフック呼び出しのまとまりが2つのコンポーネントに現れたとき、または1つのコンポーネントのエフェクトのロジックが長く、名前を付けたほうがわかりやすいとき(20行のリスナーより useOnlineStatus() のほうが読みやすい)に、フックを抜き出します。名前を変えるためだけに1つの useState 呼び出しをフックで包んだり、propsからstateをセットするために useEffect を呼ぶだけのフックを書いたりしないでください。その値はレンダリング中に計算します。
よくある質問
Reactのカスタムフックとは何ですか?
名前が use で始まり、1つ以上のほかのフックを呼ぶJavaScriptの関数です。stateを持つロジック(state、エフェクト、ref)をまとめるので、どのコンポーネントもその関数を呼ぶだけで再利用できます。
カスタムフックはコンポーネント間でstateを共有しますか?
いいえ。カスタムフックを呼ぶたびに、その中の useState や useEffect がそれぞれ実行されるので、各コンポーネントは独立したstateを持ちます。同じ値をコンポーネント間で共有するには、stateを持ち上げるか、コンテキストや外部ストアに入れてください。
カスタムフックの名前がuseで始まらなければならないのはなぜですか?
その名前によって、Reactのlintルールやほかの開発者に、この関数がフックを呼ぶことが伝わり、フックのルール(トップレベルで呼び、条件分岐やループの中では呼ばない)が適用されるからです。接頭辞がないと、linterは中の呼び出しを確認できません。
カスタムフックはJSXを返せますか?
何でも返せますが、JSXを返すならたいていコンポーネントとして書いたほうがよいでしょう。カスタムフックは普通、値と関数を、配列([value, toggle])かオブジェクト({ data, error, loading })で返します。
useEffectやイベントハンドラの中でカスタムフックを呼べますか?
いいえ。カスタムフックもフックなので、フックのルールに従います。コンポーネントか、別のカスタムフックのトップレベルで呼んでください。