はじめに
Reactコンポーネントを開発している際、気づくと1つのファイルが数百行に膨れ上がり、useState や useEffect、API送信ロジック、バリデーション処理などが混ざり合って視認性が著しく低下してしまうことはありませんか?
この状態は、コードの可読性を下げるだけでなく、ユニットテストの記述を困難にし、同様の処理を別の画面で再利用する際のコピペバグの温床になります。
ReactにおけるUI(見た目)とロジック(動作)を綺麗に分離し、保守性を高める最強のアプローチが 「カスタムフック(Custom Hooks)」 の作成です。本記事では、カスタムフックを作る際の設計原則と、実践的なリファクタリング手法を解説します。
1. カスタムフックとは?
カスタムフックとは、名前が use で始まる単なるJavaScriptの関数 です。
この関数の中では、Reactの標準フック(useState、useEffect、useContext など)を自由に呼び出すことができます。
なぜカスタムフックを使うのか?
- コンポーネントの軽量化 (UIとロジックの分離): コンポーネントは「HTMLの見た目を返す(宣言する)」ことだけに集中させ、データの取得や計算処理はフックの中に隠蔽(カプセル化)します。
- ロジックの再利用: 「スクロール位置の監視」「APIフェッチ」「フォーム入力制御」などの汎用ロジックを、複数の異なるコンポーネント間で共有できます。
2. 実践:カスタムフックへのリファクタリング例
「APIからユーザーリストを取得して画面に表示する」コンポーネントを例に、リファクタリングの流れを見てみましょう。
避けるべきコード(UIの中にロジックが埋め込まれている)
import { useState, useEffect } from 'react';
export function UserList() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('https://api.example.com/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setIsLoading(false);
})
.catch(err => {
setError(err);
setIsLoading(false);
});
}, []);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error occurred.</p>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
- 課題: このコンポーネントは、APIのURL、フェッチの処理、エラーハンドリングの状態、そして描画処理(HTML)のすべてを知りすぎています。テストを書く際も、モックの準備が複雑になります。
改善後のコード(カスタムフックの抽出)
まず、非同期データ取得ロジックだけを useUsers という名前のカスタムフックとして独立したファイルに切り出します。
// useUsers.js (カスタムフック)
import { useState, useEffect } from 'react';
export function useUsers() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let active = true;
fetch('https://api.example.com/users')
.then(res => res.json())
.then(data => {
if (active) {
setUsers(data);
setIsLoading(false);
}
})
.catch(err => {
if (active) {
setError(err);
setIsLoading(false);
}
});
// クリーンアップ関数(競合状態の防止)
return () => { active = false; };
}, []);
// コンポーネントが必要とするデータと状態だけを返却する
return { users, isLoading, error };
}
切り出したカスタムフックを使用して、元のコンポーネントを書き換えます。
// UserList.jsx
import { useUsers } from './useUsers';
export function UserList() {
// カスタムフックを呼び出すだけ
const { users, isLoading, error } = useUsers();
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error occurred.</p>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
- 結果: コンポーネントはたった数行の非常に見通しの良い記述になりました。「APIをどう叩いているか」をコンポーネント自身が気にする必要がなくなり、テストも
useUsersフックの単体テストと、UserListの描画テストに分けてシンプルに記述できるようになります。
3. 良いカスタムフックを設計するための3ルール
ルール1: UI(HTMLタグ)を返さない
カスタムフックはロジックを提供するものであるため、戻り値にはオブジェクト、配列、数値、真偽値、関数などを返します。JSX(<div /> など)をフックから返してしまうと、コンポーネントとしての再利用性が失われるため避けてください。
ルール2: 出力(返却値)は最小限にする
フック内で定義した状態変数をすべてコンポーネントに返す必要はありません。コンポーネントが描画のトリガーや制御に必要とする最低限のプロパティ(例: data, actions など)だけを厳選してリターンします。
ルール3: フック同士をネストして組み合わせる
カスタムフックの中で別のカスタムフックを呼び出すことができます。例えば、useUsers の中で汎用的な useFetch フックを呼び出すといった階層構造を作ることで、コードの共通化をさらに進めることができます。
まとめ
Reactのカスタムフックは、コンポーネントをクリーンかつテストしやすく保つための設計手法です。
- コンポーネントのコード量が肥大化したら、ロジックを
useから始まる関数へ切り出す - 状態とそれを更新する関数(Action)をカプセル化して返却する
- 副作用(
useEffect)や競合状態の対策コードをフック内に閉じ込め、コンポーネント側を安全にする
カスタムフックを活用し、長期的な運用の変化に強い、綺麗なReactプロジェクトを構築していきましょう。
