副作用に依存したコンポーネントの構造的リスク
はじめに
ReactではuseEffect
を使うことで副作用(サーバー通信、DOM操作、タイマー処理など)をコンポーネント内に定義できます。
一見便利に見えるこの設計は、実はコンポーネントが副作用に強く依存した構造を持つことによって、責務の混在や再利用性の低下、テストの困難性を引き起こします。
この記事では、レビューアーとして副作用に依存しすぎた構造をどう見抜き、どう指摘するべきかを整理します。
副作用が責務を侵食するとはどういうことか
以下のような構造は、典型的な「副作用に責務が侵食されたコンポーネント」です。
- useEffectの中にfetchやsetTimeout、イベント登録などが混在している
- ステート更新のトリガーが全てuseEffectに依存している
- 見た目とは関係のないドメイン処理や監視処理をuseEffectで持っている
これらのパターンは「描画」「データ取得」「初期化」「ロジック」の責務を区別せずに扱っている状態であり、レビューの際にはまずそこを分離できるかを判断する必要があります。
副作用に依存しすぎた構造の例
export const NotificationPanel = () => {
const [notifications, setNotifications] = useState([]);
useEffect(() => {
const timer = setInterval(() => {
fetch('/api/notifications')
.then(res => res.json())
.then(setNotifications);
}, 10000);
return () => clearInterval(timer);
}, []);
return (
<div>
{notifications.map(n => (
<p key={n.id}>{n.message}</p>
))}
</div>
);
};
@Reviewerデータ取得と定期実行、描画を全て1コンポーネント内に持っています。責務が集中しており、描画側が副作用に依存して壊れやすい構造です。データ取得部分をカスタムフックやコンテナに分離できないか検討しましょう。
この例では、描画と定期データ取得が同一の関数スコープ内で混在しており、変更のたびに全体のロジックに影響を及ぼします。
初期化ロジックが内部化している構造
副作用による初期化処理がコンポーネントに内包されると、再利用可能性が著しく下がります。
export const AuthStatus = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const token = localStorage.getItem('authToken');
if (token) {
fetch('/api/me', { headers: { Authorization: `Bearer ${token}` } })
@ReviewerlocalStorageの読み込みやAPIリクエストが内部に強く固定されています。プレゼンテーションと初期化を分離できる構造に改善可能です。 .then(res => res.json())
.then(setUser);
}
}, []);
return <div>{user ? `ようこそ ${user.name} さん` : 'ログインしてください'}</div>;
};
このようなケースでは、「初期化」と「表示」を切り分けて構造化することで、可読性と再利用性が向上します。
データ取得を外部化する構造
副作用に関わる処理を外に出す設計の一例がカスタムフックの利用です。
const useNotifications = () => {
const [notifications, setNotifications] = useState([]);
useEffect(() => {
const interval = setInterval(() => {
fetch('/api/notifications')
.then(res => res.json())
.then(setNotifications);
}, 10000);
return () => clearInterval(interval);
}, []);
return notifications;
};
export const NotificationPanel = () => {
const notifications = useNotifications();
return (
<div>
{notifications.map(n => (
<p key={n.id}>{n.message}</p>
))}
</div>
);
};
この構造では、描画ロジックは表示責務に集中し、副作用は独立して再利用可能な形で分離されています。
副作用侵食と責務分離の対比構造

useEffectの依存配列による誤動作リスク
副作用に依存する構造では、依存配列の書き方ひとつで挙動が変わるため、構造的に信頼できる設計が求められます。
useEffect(() => {
fetchData(input); // inputが変わるたびに実行すべき
}, []); // 実行されない
@ReviewerこのuseEffectはinputに依存しているにもかかわらず、依存配列に指定されていません。意図しない挙動が起こる可能性があります。
副作用依存の構造では、依存配列の正確さ=構造の信頼性です。レビューアーはそこに注目する必要があります。
状態管理との境界にも注意
副作用によって取得された値が状態管理と接続されていない場合、値の整合性が保てない設計になります。
Reduxなどを導入していない場合でも、「副作用→props更新→再描画」の流れが明確に設計されているか確認することが重要です。
useEffectを複数設置して責務を分離する
副作用の処理が複数存在する場合、ひとつのuseEffectにまとめるのではなく、処理の責務ごとにuseEffectを分割するのが望ましい設計です。
この構造により、それぞれの副作用が何を目的にしているかが明示され、レビュー時にも読み解きやすくなります。
useEffect(() => {
// 初回データ取得
fetchData();
}, []);
useEffect(() => {
// ウィンドウリサイズ時の処理
const handleResize = () => {
console.log('resized');
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
このように useEffect
を分けることで、それぞれの依存関係や処理目的が明確になり、無駄な再実行や責務の混在を避けることができます。
処理の粒度が小さくても、依存配列や意味が異なる場合は、関数内に詰め込まず、明示的に分けて記述する方が構造的に安全です。
@Reviewer: 初期データ取得とイベント登録がひとつのuseEffectに混在しています。目的の異なる副作用処理はuseEffectを分割することで、構造の可読性と再利用性を高められます。
useEffectを複数書くこと自体には制限はなく、むしろReactでは副作用ごとにuseEffectを書くのが基本設計とされています。
この設計方針を前提にレビューすることで、構造の責務が読み取りやすくなり、レビュー効率も大幅に改善します。
レビュー観点まとめ
副作用が責務を侵食している構造は、以下の視点でチェックすることが求められます。
- useEffectに複数の処理(fetch、監視、初期化など)が混在していないか
- 表示コンポーネントに副作用が内包されていないか
- カスタムフックやラップされたコンテナで副作用の責務を分離できないか
- useEffectの依存配列が状態やpropsと整合しているか
- ロジックが時間や状態の変化に強く依存していないか
副作用を「使えるから使う」のではなく、「どこに持たせるべきか」という設計意図のもとにレビューすることが、構造の健全性を保つ鍵になります。