useEffectに複数処理を詰め込んでいないか?副作用分離と責務の明示
Reactコンポーネントのレビューで useEffect
に複数の処理が詰め込まれているケースを目にすることは少なくありません。
たとえばデータ取得とローカルストレージ操作、あるいはイベント登録とログ出力など、異なる責務の処理が1つの副作用スコープに共存している状態です。
これはいわば「副作用のスパゲッティ化」であり、テスト困難性・再利用性の低下・バグの温床という形で構造的な問題を引き起こします。
useEffectが担う「副作用」とは何か
Reactにおける useEffect
は、DOMの描画後に非同期処理やサブスクリプションの登録、外部リソースへのアクセスといった「副作用処理(side effect)」を定義するための仕組みです。
この副作用処理のスコープが肥大化すること自体が、設計レビュー上のチェックポイントとなります。
副作用(side effect)とは、関数の実行によって外部状態を変更する処理全般を指します。例としてはAPI呼び出し、イベントリスナーの登録、ログ出力、状態の更新などが含まれます。
useEffectの中に処理が詰め込まれる原因
複数の副作用が一つの useEffect
に押し込まれる背景には、以下のような傾向があります。
- 処理を「画面が描画されたタイミングで全部まとめてやる」という一括的な実装思考
- useEffect自体の数を増やすと「管理が複雑になる」と誤認している
- フックの責務を明示せず、単一関数で複数処理を同時に扱ってしまう
特に中〜大規模プロジェクトでは、これが積み重なることでコードが「どの処理が何に対応しているのか分かりにくい」という状態に陥ります。
よくあるコード例とレビューコメント
以下の例を見てみましょう。
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(setProducts);
localStorage.setItem('visitCount', String(count + 1));
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
@ReviewerAPI取得・ローカルストレージ更新・イベント登録という異なる副作用が混在しています。各処理はトリガー条件や責務が異なるため、useEffectを分離して責任範囲を明確化してください。
レビュー時には、責務の種類とトリガー条件の独立性に着目することが重要です。
すべてがマウント時に一度だけ実行される処理であっても、それが同じ理由で一括化されているとは限りません。
改善例:useEffectを複数に分ける
以下は、同じロジックを責務ごとに useEffect
を分離した改善例です。
// データ取得用の副作用
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(setProducts);
}, []);
// visitCountの更新
useEffect(() => {
localStorage.setItem('visitCount', String(count + 1));
}, [count]);
// ウィンドウサイズ監視
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
@Reviewer各useEffectが責務ごとに分離されており、メンテナンス性・テスト性が向上しています。
コンポーネントの責務設計としての指摘
副作用の混在は、結果的にコンポーネント自体の責務過多に直結します。
以下の構造図では、useEffectの設計がコンポーネント設計にどのように影響するかを示しています。
フックの抽出という選択肢
より明示的に副作用の責務を切り出すには、カスタムフック化が有効です。以下のように、データ取得処理を useFetchProducts
として抽出できます。
function useFetchProducts() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(setProducts);
}, []);
return products;
}
カスタムフック化により、副作用の責務が再利用可能な単位で分割され、コンポーネント側の関心が明確になります。
状態管理を導入するべきか?
ここで迷うのが、「この責務分離をReduxやRecoilのような状態管理で行うべきか?」という問いです。
結論から言えば、「useEffectに処理を詰め込むかどうか」と「状態管理ツールの選定」は別問題です。
useEffectが責務の分離をできていない状態で、Reduxを導入しても構造的な複雑性は解消されません。
ただし、非同期処理や永続化された状態がアプリ全体に関与する場合は、状態管理ツールの検討が意味を持ちます。
まとめとレビュー視点でのポイント
useEffect
の中に異なる種類の副作用(API・DOM・localStorageなど)を混在させない- 責務・トリガー・スコープが異なる処理は別のuseEffectに分離
- 処理が一定以上に増えたら、カスタムフックへの分離を検討
- コンポーネントの役割が肥大化していないかを定期的に見直す
レビューアーは「useEffectの中身」だけを見るのではなく、その構造がコンポーネント全体にどう影響しているかという視点を持つことが求められます。