複数のuseEffectを分けすぎて構造破綻していないか?
Reactにおける useEffect
のベストプラクティスとして「責務が異なる副作用は別々のEffectに分けるべき」という方針が知られています。これは構造的にも正しいアプローチですが、過度に細分化された useEffect
はかえって構造の意図を曖昧にし、レビューや保守を困難にする原因になります。
本記事では、useEffect
の分割と統合のバランスをレビューアー視点でどう見極めるかを、構造設計の観点から整理します。
分割が有効に働く条件とその目的
まず、useEffect
を分ける目的は大きく以下の2つです:
- 副作用のトリガー条件が異なる(例:Aは初期化、Bは状態変更時)
- 副作用の対象が異なる(例:一方はDOM操作、他方はAPI通信)
この条件に該当する場合、useEffect
を分けることで依存配列を最小限に保ち、構造の意図が明確になるというメリットがあります。
責務ごとに分割された良い例
useEffect(() => {
// 初回のみ
initFeature();
}, []);
useEffect(() => {
// 設定が変更されたときのみ
updateFeature(flag);
}, [flag]);
コードレビュー
@Reviewer: 初期化と動的更新で分けられており、依存配列も最小で構造が明快です。
過剰分割によって構造が破綻するパターン
パターン1:1つのロジックが複数のEffectにまたがって記述されている
副作用が分散して見通せない構造
useEffect(() => {
if (step === 1) prepare();
}, [step]);
useEffect(() => {
if (step === 2) execute();
}, [step]);
useEffect(() => {
if (step === 3) finalize();
}, [step]);
コードレビュー
@Reviewer: `step` に対する処理が3つのEffectに分かれていることで、構造上の連続性や状態の変遷が読み取りづらくなっています。単一のEffectに統合した方が構造として自然です。
条件分岐の流れや順序が副作用の中に分散してしまうと、状態のトランジションが読みづらくなり、テストや保守も困難になります。
統合が望ましいケースの例
処理が明確に関連しており、同じ依存条件で動く場合
統合した方が構造が明確な例
useEffect(() => {
if (step === 1) {
prepare();
} else if (step === 2) {
execute();
} else if (step === 3) {
finalize();
}
}, [step]);
コードレビュー
@Reviewer: `step` という状態の変遷を1つのEffectで管理する構造にすることで、遷移の全体像と処理の意図が読みやすくなっています。
可視化:過剰分割による副作用の分散
パターン2:依存配列が不必要に複雑化する構造
副作用を分割することで、それぞれに対して依存配列を個別に記述する必要が生じ、変更時の追跡コストが上がるリスクがあります。
過剰分割による依存配列の冗長化
useEffect(() => {
fetchData();
}, [user.id, config.token]);
useEffect(() => {
validateData();
}, [config.token, user.role]);
コードレビュー
@Reviewer: どちらのEffectも`config.token`を依存に含んでおり、冗長です。副作用の粒度が重複しており、統合によって構造が整理される可能性があります。
分割・統合の判断基準(レビュー観点)
判断項目 | 統合が望ましいケース |
---|---|
依存対象 | 同じ変数に依存している |
処理目的 | 処理の流れが連続している |
副作用 | 同一コンテキストで実行されるべき |
読みやすさ | 分割により読みにくくなっている |
構造的に見たとき、「分けたことで本当に見通しが良くなったか?」という観点を持つことが重要です。
カスタムHookに分ける判断との違い
useEffect
を分けるのではなく、「責務ごとにHookに切り出す」ことで構造が改善されるケースもあります。
カスタムHookでの責務分離
function useUserInitializer(userId: string) {
useEffect(() => {
fetch(`/api/user/${userId}`);
}, [userId]);
}
利用側
useUserInitializer(user.id);
コードレビュー
@Reviewer: 依存条件が固定されており、責務が明確です。呼び出し元のコンポーネントの構造を簡素に保てています。
このように、useEffectを分けるより、責務単位で抽象化する方が構造上の透明性が高まるケースは多くあります。
レビュー時の確認ポイント
副作用の分割はあくまで責務分離の手段であり、目的ではありません。レビューアーは「分けられた構造が本当に設計として正しいのか」を常に疑い、構造として読みやすく、変更に強い設計になっているかを判断する必要があります。