Reactのカスタムフックの作り方を完全ガイド!再利用可能なロジックを切り出す仕組み
生徒
「カスタムフックって何ですか?難しそうに見えます。」
先生
「Reactのフックを自分でまとめた関数のことです。複数のコンポーネントで使うロジックを切り出して再利用できます。」
生徒
「例えばどんなときに使いますか?」
先生
「フォームの入力状態、API取得の共通処理、表示の切り替えなど、同じ処理を何度も書きたくないときに便利です。」
生徒
「作るときの注意点はありますか?」
先生
「名前をuseで始めること、副作用のクリーンアップ、依存配列の管理などをしっかり考えましょう。」
1. カスタムフックとは?やさしい説明
カスタムフックは、ReactのuseStateやuseEffectなどのフックを組み合わせて作る「再利用可能なロジック」の塊です。仕事で言えば「道具箱」に似ています。道具箱の中にネジ回しやハンマーをまとめておけば、どの現場でも同じ道具をすぐに取り出せます。コンポーネントは現場、カスタムフックは共通の道具箱と考えると分かりやすいです。
もう少し具体的に言うと、画面の表示に関わる見た目(UI)はコンポーネントが担当し、データの取得や状態の管理、タイマーや購読などの「振る舞い」はカスタムフックに任せます。こうすることでコンポーネントは読みやすく、テストもしやすくなります。
重要なポイント:
- 名前は「use」で始める(例:
useToggle)。これはReact側がフックとして扱うための慣習です。 - 内部で他のフックを使える。例えば
useStateやuseEffect、useRefなど。 - UIではなく、ロジック(状態管理や副作用)を返す。見た目の要素は返さないのが基本です。
2. カスタムフックを作るメリット
主なメリットは下記のとおりです:
- コードの重複を減らせる(DRY原則)。
- テストや保守がしやすくなる。ロジックが分離されているとユニットテストが書きやすいです。
- コンポーネントがシンプルになる。見た目に集中でき、読みやすくなります。
たとえば複数の画面で同じAPI呼び出しや状態管理を行う場合、カスタムフックにまとめることでバグ修正や仕様変更の対応が一箇所で済みます。また、新しい画面を作るときもフックを呼び出すだけで共通機能が使えるため開発効率が上がります。
3. 簡単なカスタムフックの作り方(useToggle)
まずは最もシンプルな例、トグル(true/falseを切り替える)を作りましょう。短くて理解しやすいので初めてのカスタムフックに最適です。
import { useState, useCallback } from "react";
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
このカスタムフックは、value(現在値)と切り替え関数を返します。コンポーネント側は状態の詳細を知らずに使えます。
4. 実際に使ってみる(useToggleをコンポーネントで利用)
import React from "react";
import { useToggle } from "./useToggle";
function ToggleExample() {
const { value, toggle } = useToggle(false);
return (
<div>
<p>状態: {value ? "ON" : "OFF"}</p>
<button onClick={toggle}>切り替え</button>
</div>
);
}
export default ToggleExample;
5. API取得のカスタムフック(useFetchの例)
次は少し実用的な例です。APIからデータを取得する共通処理をカスタムフックにします。エラーハンドリングや読み込み状態もまとめられます。
import { useState, useEffect } from "react";
export function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
setLoading(true);
fetch(url)
.then(res => res.json())
.then(json => { if (isMounted) setData(json); })
.catch(err => { if (isMounted) setError(err); })
.finally(() => { if (isMounted) setLoading(false); });
return () => { isMounted = false; };
}, [url]);
return { data, loading, error };
}
このフックを使えば、コンポーネントは表示ロジックだけに集中できます。読み込み中はスピナーを出し、エラーがあればエラーメッセージを表示する、といった共通処理を簡潔に扱えます。
6. カスタムフック作成の注意点とベストプラクティス
- 名前はuseで始める:Reactは名前でフックか判断します。
- 副作用のクリーンアップ:useEffectでタイマーや購読を行う場合は必ずクリーンアップを返す。
- 状態を返す形:配列よりオブジェクトで返すと拡張しやすい(順序に依存しない)。
- 依存配列に注意:useCallbackやuseEffectの依存を正しく指定しないと古い値を参照するバグが出ます。
- 副作用の分離:API取得やローカルストレージ書き込みなどは別々の小さなフックに分けると再利用性が高まります。
設計のコツとしては、フックは一つの責務に絞ることです。例えば「データ取得」と「フォームのバリデーション」は別々のフックに分けると後で組み合わせて使いやすくなります。
7. リファクタ手順:既存コンポーネントからフックを切り出す流れ
- 共通するロジックを探す。状態や副作用が重複している箇所を見つける。
- そのロジックだけを抜き出して関数(useで始まる名前)にまとめる。
- 依存する値や関数を引数として受け取り、必要な状態と操作を返す。
- テストを書いて動作を確認する。ユニットテストはバグを防ぎます。
- コンポーネント側をシンプルにし、フックを呼び出して動作を確認する。
この手順を踏むと安全にリファクタリングできます。最初は小さなフックから始めて段階的に分割するとリスクを減らせます。
8. 実務での使い分けとパターン
実務では以下のようなパターンでカスタムフックを作ることが多いです:
- 状態管理用フック(トグル、フォーム、モーダル管理など)
- データ取得用フック(API取得、キャッシュ、再試行など)
- ユーティリティフック(ウィンドウサイズの監視、スクロール位置取得など)
フォルダ構成としては、src/hooks配下にフックを置き、命名規則やドキュメントを整備するとチームで使いやすくなります。READMEで使い方や返り値の説明を記載しておくと、新しい開発者が理解しやすいです。
9. 依存配列とメモ化、パフォーマンスについて
useEffectやuseCallbackの依存配列は重要です。依存を漏らすと最新の値を使えず古い値が残るバグにつながります。反対に依存を過剰に指定すると不要な再実行が起きてパフォーマンスが悪化します。eslintのルールreact-hooks/exhaustive-depsを有効化すると依存忘れを防げます。
また、計算が重い処理はuseMemoでメモ化することで再レンダリング時の負荷を減らせますが、メモ化にもコストがあるため本当に必要な箇所だけを対象にしてください。レンダリング回数が多い箇所やデータ量が大きい処理を中心に検討しましょう。
10. メモリリーク対策と中止処理
API取得などでコンポーネントがアンマウントした後に状態を更新するとメモリリークや警告が出ます。fetchや非同期処理にはAbortControllerを使って中止する方法や、クリーンアップでフラグを切る方法があります。最近はAbortControllerが推奨される場面が増えています。
また、WebSocketや購読を行う場合は必ず購読解除を行い、タイマーを設定する場合はclearTimeoutやclearIntervalで解除してください。これを怠るとページを離れた後も処理が残りパフォーマンス問題を引き起こします。
11. useLocalStorageの例
ローカルストレージを扱うユーティリティフックの例を示します。値の読み書きを共通化するとコードがすっきりします。
import { useState, useEffect } from "react";
export function useLocalStorage(key, initialValue) {
const [state, setState] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(state));
}, [key, state]);
return [state, setState];
}
12. ステールクロージャ(古い値参照)の回避
よくある問題に「ステールクロージャ」があります。関数が古い状態を閉じ込めてしまい、思ったとおりに動かないケースです。これを避けるには依存配列を正しく設定するか、関数内で最新の値を参照する方法に切り替えるとよいです。例えば状態更新に関しては関数型更新(setState(prev => ...))を使うと安全です。
13. ドキュメント化とチーム運用
フックは再利用のために作るので、READMEやコメントで引数や返り値、エッジケースを明記しておきましょう。チームで命名規則や返却形式を統一しておくと、新しい人が使いやすくなります。
14. よくある疑問Q&A
Q: カスタムフックでDOM操作はできますか?
A: 基本的にカスタムフックはロジック用ですが、useRefを返してコンポーネント側でDOMを操作する設計も可能です。UI固有の処理はコンポーネント側に残すと役割が明確になります。
Q: クラスコンポーネントでも使えますか?
A: フックは関数コンポーネント専用のため、クラスコンポーネントでは使えません。移行を検討する際は段階的なリファクタをおすすめします。
15. 最後に注意点
カスタムフックは強力ですが、乱用すると抽象化が進みすぎて理解しづらくなります。名前や責務、返り値の形を揃えてチームでルールを作るとよいでしょう。まずは小さなフックを作って慣れていくことをおすすめします。
まとめ
ここまで、Reactのカスタムフックについてさまざまな角度から学んできました。カスタムフックは、見た目を描画するコンポーネントとは違い、状態管理や処理の流れといった「ロジック部分」をきれいに切り出して再利用しやすくするための仕組みです。特に、同じ処理を複数のページやコンポーネントで使いたいとき、そのロジックを毎回書き直す必要がなくなり、コードの見通しが良くなるという大きな利点があります。また、ロジックがコンポーネントから離れることで、UI部分がとても読みやすくなり、保守性も向上します。複雑な画面やデータ取得が多いアプリケーションほど、カスタムフックの役割は重要になります。 さらに、カスタムフックは名前をuseで始めること、依存配列や副作用の扱いに注意するなど、React特有のルールに従って設計することが求められます。うまく作れれば、アプリ全体で共通化された「使い回せる道具箱」のような存在になり、開発効率が飛躍的に向上します。たとえば、フォームの入力状態を管理するフック、データ取得のフック、モーダルの表示非表示を切り替えるフックなど、多くの開発現場で共通ニーズがある機能はカスタムフック化することで扱いやすくなります。 また、カスタムフックはJavaScriptの関数として作られるため、UIの描画ロジックを含まず、ロジック単体でのテストが書きやすいというメリットもあります。ユニットテストが整備されれば、機能追加や仕様変更の際にも安心して修正ができます。アプリが大規模になればなるほど、この仕組みは強力に作用します。 以下には、記事全体の内容を踏まえた、少し応用的なサンプルを紹介します。状態の切り替え、ローカルストレージとの連携など、実務でよくある処理を一つのフックにまとめた例です。
総合サンプル:複数の機能をまとめたカスタムフック例
import { useState, useEffect, useCallback } from "react";
export function useCustomFeature(key, initialValue = false) {
const [value, setValue] = useState(initialValue);
const [count, setCount] = useState(0);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
// ローカルストレージへ保存
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [value, key]);
const increment = () => setCount(c => c + 1);
return { value, toggle, count, increment };
}
import React from "react";
import { useCustomFeature } from "./useCustomFeature";
function DemoComponent() {
const { value, toggle, count, increment } = useCustomFeature("demo-key");
return (
<div>
<p>トグル状態: {value ? "ON" : "OFF"}</p>
<button onClick={toggle}>切り替え</button>
<h3>カウント: {count}</h3>
<button onClick={increment}>カウントを増やす</button>
</div>
);
}
export default DemoComponent;
この例のように、一つのフックに複数のロジックを組み合わせることもできます。ただし、やりすぎは禁物で、役割が不明確にならないよう「一つのフックには一つの責務」という考え方を大切にしましょう。必要に応じてフック同士を組み合わせることで、より柔軟な構成にできます。 カスタムフックを使いこなすと、Reactの設計力が大幅に向上し、アプリ全体の品質と開発スピードが安定します。コンポーネントの肥大化も防げるため、長期的なメンテナンスでも効果を発揮します。小さなフックからでもいいので、ぜひ実際に作りながら理解を深めていきましょう。
生徒
「カスタムフックって難しそうだと思っていたけれど、よく使う処理をまとめるだけなんですね!」
先生
「その通りです。Reactのフックを組み合わせて、自分専用の便利な関数にするイメージですよ。」
生徒
「同じ処理を繰り返し書かなくてよくなるのは助かりますね。コードも読みやすくなりそうです。」
先生
「慣れてくると、どのロジックを切り出すべきか判断できるようになります。小さなところから始めれば大丈夫ですよ。」