【実務・中級編】 カスタムフックへの状態ロジックの抽出と再利用 – React実践ガイド

お疲れ様です。最近、どのコードレビューを見ても「コンポーネントが肥大化して神コンポーネント化している」「似たような非同期データのフェッチ処理があちこちにコピペされている」という技術負債の山に直面していませんか?

中級からシニアへとステップアップするフェーズにおいて、最も差がつくスキルの一つが「状態ロジックの適切な分離と再利用」です。

今回は、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(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

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

エラー: {error}

;
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(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

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

エラー: {error}

;
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開発、一緒に楽しんでいきましょう!

コメント

タイトルとURLをコピーしました