状態・副作用・ユーティリティを一緒にしたカスタムHookの問題点
カスタムHookの利用が一般化した反面、状態・副作用・ユーティリティをすべて一括で内包した構造も目立つようになっています。このようなHookは一見便利に見えますが、レビューアー視点では責務の集中・見通しの悪化・構造の分解困難といった問題を多く孕んでいます。
本稿では、状態・副作用・ユーティリティ処理が混在しているカスタムHookの構造に着目し、レビューアーがどのように問題点を見抜き、改善案を提案すべきかを解説します。
状態・副作用・ユーティリティが混在する例
function useUserAccessControl() {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetch("/api/user")
.then(res => res.json())
.then(setUser)
.catch(setError);
}, []);
const isAdmin = user?.role === "admin";
const canEdit = isAdmin && user?.permissions.includes("edit");
const formatUserName = (user) => user?.name?.toUpperCase();
return { user, isAdmin, canEdit, formatUserName, error };
}
@Reviewer状態管理(user, error)、副作用(fetch)、ユーティリティ(formatUserName)が同一Hookに混在しています。責務ごとにHookを分割することで構造の見通しが向上します。
このような構造は、責務単位での再利用やテストが難しくなるだけでなく、他の開発者が「どこを編集すれば何が変わるか」を判断しにくくなります。
Hookの構成要素を分類する
レビューではまず、カスタムHookの中にどのような構成要素が含まれているかを分類して読み解くと、責務の混在が明確になります。
要素分類 | 主な処理例 | Hook内の位置 |
---|---|---|
状態管理 | useState , useReducer |
状態保持ロジック |
副作用処理 | useEffect , useLayoutEffect |
非同期取得・購読・解除 |
計算・ユーティリティ | 条件判定・フォーマット関数 | returnに含まれるロジック |
これらがすべて1つのHookに含まれている場合、分割の検討対象となります。
責務の分割による改善例
function useUserInfo() {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetch("/api/user")
.then(res => res.json())
.then(setUser)
.catch(setError);
}, []);
return { user, error };
}
function useUserPermission(user) {
const isAdmin = user?.role === "admin";
const canEdit = isAdmin && user?.permissions.includes("edit");
return { isAdmin, canEdit };
}
function useFormatUser() {
const formatUserName = (user) => user?.name?.toUpperCase();
return { formatUserName };
}
const { user, error } = useUserInfo();
const { isAdmin, canEdit } = useUserPermission(user);
const { formatUserName } = useFormatUser();
@Reviewer構造が責務単位で整理されており、それぞれのHookが独立して意味を持っていますね。デバッグや再利用、テストの単位が明確で、構造的な見通が良いですね!。
関数の返却数が多すぎないかを確認
次のような構造は責務が過剰に混在している兆候です。
const {
user,
error,
isAdmin,
canEdit,
isLoggedIn,
isExpired,
formatUserName,
resetUser,
updateAccess,
} = useAuthLogic();
@Reviewer返却される値や関数が多く、Hookの内部に複数の責務が混在している可能性が高いです。構造が不明瞭で、機能の追跡や責任範囲の把握が困難です。
返却するプロパティが7個以上になっている場合、内部の責務を明示的に分離するか、呼び出し元の構造を再設計する必要があります。
ユーティリティをHookに含める必要があるか?
フォーマット処理や単純な変換関数は、Hookに含めるよりも独立したユーティリティ関数として分離する方が構造上明確になります。
// utils/user.ts
export function formatUserName(user) {
return user?.name?.toUpperCase();
}
Hook内で副次的に生成された処理が本質的に状態とは関係ないものであれば、それをHookの一部に含める必要はありません。
@Reviewer format関数のような純粋関数はHook内に置かず、ユーティリティに切り出した方が構造が明快です。責務の範囲が明示され、再利用性も向上します。
状態と副作用の結合が密すぎないか
たとえば、useEffect
で複数のAPIを呼び、複数の状態を更新するような構造では、副作用単位でHookを分けるべきです。
useEffect(() => {
fetchA().then(setA);
fetchB().then(setB);
fetchC().then(setC);
}, []);
複数の非同期処理が1つの副作用内で密結合しています。副作用ごとに責務を分けることで、失敗時のハンドリングや依存の切り分けが容易になります。
まとめ:レビューアーの観点チェックリスト
- 状態・副作用・ユーティリティが混在していないか
- Hook名がすべての責務を内包していないか(例:useSomethingAll)
- return値が多すぎて呼び出し元の責務が不明瞭になっていないか
- 処理単位で再利用・テスト可能な単位に分割できないか
- 本当にHookに含めるべき処理なのか(ユーティリティ関数の切り出し検討)
カスタムHookは「なんでも詰め込める場所」ではありません。レビューアーの役割は、ロジックと構造の分離が設計的に正当化されているかを見極め、構造の明瞭さと保守性を守ることにあります。