お疲れ様です。最近、どのコードレビューを見ても「コンポーネントが肥大化して神コンポーネント化している」「似たような非同期データのフェッチ処理があちこちにコピペされている」という技術負債の山に直面していませんか?
中級からシニアへとステップアップするフェーズにおいて、最も差がつくスキルの一つが「状態ロジックの適切な分離と再利用」です。
今回は、Reactの基本でありながら実務で最も事故りやすい `useState` の非同期バグや副作用の沼を回避しつつ、コンポーネントをスッキリと保つ「カスタムフック(Custom Hooks)」へのロジック抽出について、現場のリアルな知見を交えて徹底解説していきます。
—
1. なぜカスタムフックが必要なのか?(現場の泥臭い現実)
Reactの開発初期によくあるアンチパターンとして、ひとつのコンポーネントの中に `useState` が5個も6個も並び、さらに `useEffect` が複雑に絡み合っているコードを見たことがないでしょうか。UIの描画を担当する JSX と、データの取得・バリデーション・状態更新といったビジネスロジックが密結合している状態です。
これの何が問題か?
1. テストが困難: UIとロジックがベタ書きされているため、純粋なロジック単位での単体テスト(Unit Test)が書きづらい。
2. 再利用性の欠如: 似たようなモーダルの開閉やフォームの入力制御を、別の画面で実装するときに「コピペ」が横行する。
3. 認知負荷の爆発: コンポーネントを開いた瞬間に「何をやっているコンポーネントなのか」がひと目で分からない。
カスタムフックは、このカオスを綺麗に整理整頓するための「状態を持ったロジックのカプセル化ツール」です。
—
2. Reactが裏側でやっていること:フックの本質
ここで一度、Reactがどのようにフックを管理しているか、その裏側の仕組み(メンタルモデル)を整理しておきましょう。
Reactの `useState` や `useEffect` は、内部的には「ファイバー(Fiber)ノードに紐づく連結リスト(Linked List)」として順序管理されています。
つまり、カスタムフックというのは、「Reactのフック呼び出しのルール(トップレベルで呼び出す、条件分岐内で呼ばない等)さえ守っていれば、複数のフックを一つのパッケージにまとめて別の場所に置いても、Reactは全く同じように状態を追跡できる」という性質を利用した、単なるJavaScriptの関数に過ぎません。
マジックでも何でもなく、単なる「関数の抽出(Extraction)」です。だからこそ、クリーンで強力なのです。
—
3. 実践!肥大化したコンポーネントからロジックを抽出する
では、実務でよくある「ローディング状態、エラーハンドリング、データフェッチ」を伴う非同期処理を例に、具体的なコードを見ていきましょう。
まずは、やってはいけない(あるいはメンテナンスで苦しむ)アンチパターンのコンポーネントから。
❌ 改善前のコード(ロジックがベタ書きされたコンポーネント)
import React, { useState, useEffect } from ‘react’;
// ユーザー情報の型定義
type User = {
id: number;
name: string;
};
export const UserProfileCard = ({ userId }: { userId: number }) => {
// 状態がコンポーネント内に散らばっている
const [user, setUser] = useState
const [loading, setLoading] = useState
const [error, setError] = useState
useEffect(() => {
let isMounted = true; // メモリリーク対策のフラグ
setLoading(true);
setError(null);
fetch(`/api/users/${userId}`)
.then((res) => {
if (!res.ok) throw new Error(‘データの取得に失敗しました’);
return res.json();
})
.then((data) => {
if (isMounted) {
setUser(data);
}
})
.catch((err) => {
if (isMounted) {
setError(err.message);
}
})
.finally(() => {
if (isMounted) {
setLoading(false);
}
});
return () => {
isMounted = false; // クリーンアップ
};
}, [userId]);
if (loading) return
;
if (error) return
;
if (!user) return
;
return (
{user.name}
{/ 実際のUIコードが続く… /}
);
};
このコード、動くには動きますが、もし「別の画面でもユーザーデータを取得したい」となったとき、この `useEffect` や `isMounted` のお決まりのボイラープレートをそのままコピーすることになりますよね。これはシニアとしては避けたいところです。
—
⭕ 改善後のコード:カスタムフックへの抽出
それでは、このデータフェッチと非同期状態の管理ロジックを `useUser` というカスタムフックに綺麗に切り出してみましょう。
import { useState, useEffect } from ‘react’;
type User = {
id: number;
name: string;
};
// —————————————————————–
// カスタムフック: useUser
// 役割: ユーザーIDを受け取り、フェッチ状態(データ、ローディング、エラー)を返す
// —————————————————————–
export const useUser = (userId: number) => {
const [user, setUser] = useState
const [loading, setLoading] = useState
const [error, setError] = useState
useEffect(() => {
// 競合状態(Race Condition)やアンマウント後の状態更新を防ぐためのフラグ
let isMounted = true;
const fetchUser = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(‘ユーザー情報の取得に失敗しました’);
}
const data: User = await response.json();
if (isMounted) {
setUser(data);
}
} catch (err) {
if (isMounted) {
setError(err instanceof Error ? err.message : ‘予期せぬエラーが発生しました’);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchUser();
// クリーンアップ関数
return () => {
isMounted = false;
};
}, [userId]); // userIdが変わった時だけ再フェッチ
// コンポーネント側で必要になる状態と関数をオブジェクトとして返す
return { user, loading, error };
};
そして、このカスタムフックを利用するコンポーネント側は、驚くほどシンプルになります。
import React from ‘react’;
import { useUser } from ‘./useUser’; // 切り出したカスタムフックをインポート
export const UserProfileCard = ({ userId }: { userId: number }) => {
// ロジックが完全に隠蔽され、UIの表現に集中できる
const { user, loading, error } = useUser(userId);
if (loading) return
;
if (error) return
;
if (!user) return
;
return (
{user.name}
{/ ここにUIコンポーネントがスッキリと収まる /}
);
};
どうでしょう? `UserProfileCard` は「何を表示するか(UI)」だけに責任を持ち、データ取得や非同期バグ対策(`isMounted` によるメモリリーク防止など)の泥臭い処理はすべて `useUser` が裏側で引き受けてくれています。これが関心の分離(Separation of Concerns)です。
—
4. シニアが教える、カスタムフック設計の3大原則
実務でカスタムフックを書く際、チームメンバーから「これ、どう設計すればいいですか?」と聞かれたら、私は以下の3点を必ず伝えるようにしています。
1. 名前は必ず `use` で始めること
- リンター(ESLintの `eslint-plugin-react-hooks`)がフックのルールを正しく検知するために必須です。ここを外すとバグの温床になります。
2. UIを返さない、純粋な「状態とロジック」を返すこと
- カスタムフックの中にJSX(HTML風のマークアップ)をベタ書きし始めると、それはもうカスタムフックではなく「謎の巨大コンポーネント」への第一歩です。JSXはコンポーネント側に任せ、フックはあくまでデータやハンドラー関数(プレーンなJSオブジェクトや配列)を返すように徹底しましょう。
3. 引数と戻り値のインターフェースを明確に型定義する
- TypeScriptを使う醍醐味です。戻り値にオブジェクトを使うか配列を使うかはチームの好みが分かれますが、返す項目が3つ以上になる場合は名前でアクセスできるオブジェクト(`{ user, loading, error }`)形式にするのが、後からの改修でメンテしやすいのでおすすめです。
—
まとめ
カスタムフックへの状態ロジックの抽出は、単にコードを綺麗にするための「お洒落なテクニック」ではありません。チーム開発において変更容易性を高め、複雑な非同期バグから身を守るための実践的な防衛策です。
次にあなたがコードを書くとき、あるいは既存のコンポーネントの肥大化に頭を抱えたときは、「この中のどのロジックを `use…` という関数に逃がせるだろうか?」という視点を持ってみてください。コードベースの美しさと開発スピードが劇的に変わるはずです。
現場のモダンなReact開発、一緒に楽しんでいきましょう!

コメント