React開発で「なんとなくuseEffectを使っておけば動く」と思っていませんか?実は、useEffectは万能薬ではありません。むしろ、不適切な使い方をするとパフォーマンス問題や無限ループを引き起こす原因になります。
この記事では、useEffectの「使いすぎ」によくある問題から、正しい判断基準、そしてReact QueryやSWRなどモダンな代替手段まで、実践的に解説します。
目次
useEffectの「使いすぎ」でよくある問題
無限ループと依存配列の罠
useEffectの最も厄介な問題の一つが、依存配列の設定ミスによる無限ループです:// ❌ 無限ループを引き起こすコード
function BadComponent() {
const [user, setUser] = useState(null);
const [profile, setProfile] = useState({});
useEffect(() => {
if (user) {
setProfile({ name: user.name, email: user.email }); // profileが更新される
}
}, [user, profile]); // profileが依存配列にあると無限ループ
return {profile.name};
}
このコードでは、`useEffect`内で`profile`を更新し、その`profile`が依存配列に含まれているため、無限ループが発生します。
パフォーマンス劣化の原因
useEffectの乱用は、不要な再実行によりパフォーマンスを悪化させます:// ❌ 不要なuseEffectでパフォーマンス劣化
function ExpensiveComponent({ items, filter }) {
const [filteredItems, setFilteredItems] = useState([]);
useEffect(() => {
// この計算は毎回useEffectで実行される(重い処理)
const filtered = items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
setFilteredItems(filtered);
}, [items, filter]);
return (
{filteredItems.map(item => {item.name})}
);
}
複雑になりがちなコード例
useEffectを使いすぎると、コードが読みにくくなります:// ❌ useEffectが多すぎて複雑
function OverComplexComponent({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (userId) {
setIsLoading(true);
fetchUser(userId).then(setUser);
}
}, [userId]);
useEffect(() => {
if (user) {
fetchPosts(user.id).then(setPosts);
}
}, [user]);
useEffect(() => {
if (posts.length > 0) {
fetchComments(posts[0].id).then(setComments);
}
}, [posts]);
useEffect(() => {
if (comments.length > 0) {
setIsLoading(false);
}
}, [comments]);
// 複雑すぎる依存関係...
}
本当にuseEffectが必要?5つの判断基準
React公式は「You Might Not Need an Effect」で、useEffectが不要なケースを明示しています。以下の基準で判断しましょう:1. 副作用vs計算処理の区別
| 処理の種類 | 適切な実装 | useEffectが必要? |
|---|---|---|
| データフェッチ | React Query/SWR | △(ライブラリ推奨) |
| 計算・フィルタリング | useMemo/レンダー時計算 | ❌ |
| 状態の同期 | derived state | ❌ |
| イベントハンドラ | 直接関数呼び出し | ❌ |
| DOM操作 | useEffect/useLayoutEffect | ✅ |
2. レンダー中に計算できるかチェック
// ❌ useEffectで計算
function BadExample({ firstName, lastName }) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return {fullName};
}
// ✅ レンダー時に計算
function GoodExample({ firstName, lastName }) {
const fullName = `${firstName} ${lastName}`; // シンプルに計算
return {fullName};
}
3. 状態の同期vs派生状態
// ❌ useEffectで状態同期
function BadSync({ items }) {
const [selectedItem, setSelectedItem] = useState(null);
const [selectedIndex, setSelectedIndex] = useState(-1);
useEffect(() => {
const index = items.findIndex(item => item === selectedItem);
setSelectedIndex(index);
}, [items, selectedItem]);
}
// ✅ derived stateで解決
function GoodSync({ items }) {
const [selectedItem, setSelectedItem] = useState(null);
const selectedIndex = items.findIndex(item => item === selectedItem); // 計算で求める
return Selected: {selectedIndex};
}
4. イベントハンドラとの使い分け
// ❌ useEffectでボタンクリック処理
function BadEventHandling() {
const [count, setCount] = useState(0);
const [shouldIncrement, setShouldIncrement] = useState(false);
useEffect(() => {
if (shouldIncrement) {
setCount(c => c + 1);
setShouldIncrement(false);
}
}, [shouldIncrement]);
return (
);
}
// ✅ 直接イベントハンドラで処理
function GoodEventHandling() {
const [count, setCount] = useState(0);
return (
);
}
5. 初期化処理の判断
// ❌ useEffectで初期化
function BadInit() {
const [data, setData] = useState([]);
useEffect(() => {
setData(getInitialData());
}, []);
}
// ✅ useState初期値で解決
function GoodInit() {
const [data, setData] = useState(() => getInitialData()); // lazy initial state
}
useEffectの代わりになる7つのパターン
1. useMemoでの計算最適化
// ✅ useMemoで重い計算を最適化
function OptimizedComponent({ items, filter }) {
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
{filteredItems.map(item => {item.name})}
);
}
2. useCallbackでの関数メモ化
// ✅ useCallbackで関数を最適化
function ParentComponent({ items }) {
const [filter, setFilter] = useState('');
const handleItemClick = useCallback((item) => {
console.log('Clicked:', item.name);
}, []); // 依存配列が空なので関数は一度だけ作成
return (
setFilter(e.target.value)}
/>
);
}
3. Derived Stateパターン
// ✅ 複数の状態から計算で求める
function UserProfile({ user }) {
const [theme, setTheme] = useState('light');
// derived states - useEffectは不要
const isLoggedIn = Boolean(user);
const userName = user?.name || 'Guest';
const avatarUrl = user?.avatar || `/avatars/default-${theme}.png`;
const canEdit = isLoggedIn && user?.role === 'admin';
return (
{userName}
{canEdit && }
);
}
4. useState関数型更新
// ✅ 関数型更新で前の状態を参照
function Counter() {
const [count, setCount] = useState(0);
const [history, setHistory] = useState([]);
const increment = () => {
setCount(prev => prev + 1);
setHistory(prev => [...prev, prev.length + 1]); // 前の状態を使用
};
return (
History: {history.join(', ')}
);
}
5. useReducerでの複雑な状態管理
// ✅ useReducerで複雑な状態変更を一元管理
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, action.todo],
totalCount: state.totalCount + 1
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
),
completedCount: state.completedCount + (action.completed ? 1 : -1)
};
default:
return state;
}
};
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
totalCount: 0,
completedCount: 0
});
// useEffectで状態同期する必要なし
const incompleteTodos = state.todos.filter(todo => !todo.completed);
return (
Total: {state.totalCount}, Completed: {state.completedCount}
{incompleteTodos.map(todo => (
))}
);
}
6. useRefでの値の保持
// ✅ useRefで再レンダーを引き起こさない値の保持
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
const startTimer = () => {
if (intervalRef.current) return; // 既に動いている場合は何もしない
intervalRef.current = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
};
const stopTimer = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
// useEffectではなく、ボタンクリックで直接操作
return (
{seconds}秒
);
}
7. カスタムフックでの抽象化
// ✅ カスタムフックで複雑なロジックを抽象化
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(prev => !prev), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return [value, { toggle, setTrue, setFalse }];
}
function Component() {
const [isVisible, { toggle, setTrue, setFalse }] = useToggle(false);
// useEffectを使わずに状態管理
return (
{isVisible && 表示されています}
);
}
モダンなReact開発でのuseEffect以外の選択肢
React Query/TanStack Queryでのサーバー状態管理
あわせて読みたい


TanStack Query
Powerful asynchronous state management, server-state utilities and data fetching. Fetch, cache, update, and wrangle all forms of async data in your TS/JS, React...
// ❌ useEffectでデータフェッチ
function BadDataFetching({ userId }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setIsLoading(true);
setError(null);
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setIsLoading(false));
}, [userId]);
if (isLoading) return Loading...;
if (error) return Error: {error.message};
return {user?.name};
}
// ✅ React Queryでデータフェッチ
import { useQuery } from '@tanstack/react-query';
function GoodDataFetching({ userId }) {
const {
data: user,
isLoading,
error
} = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5分間キャッシュ
retry: 3
});
if (isLoading) return Loading...;
if (error) return Error: {error.message};
return {user?.name};
}
SWRでのシンプルなデータフェッチ
あわせて読みたい
SWR
React Hooks for Data Fetching.
// ✅ SWRでシンプルなデータフェッチ
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(res => res.json());
function UserProfile({ userId }) {
const { data: user, error, isLoading } = useSWR(
userId ? `/api/users/${userId}` : null, // 条件付きフェッチ
fetcher,
{
revalidateOnFocus: false,
revalidateOnReconnect: true,
refreshInterval: 30000 // 30秒ごとに再検証
}
);
if (isLoading) return Loading...;
if (error) return Failed to load user;
if (!user) return No user data;
return (
{user.name}
{user.email}
);
}
React 18のuseDeferredValueとuseTransition
// ✅ useDeferredValueで重い計算を遅延
import { useDeferredValue, useMemo } from 'react';
function SearchResults({ searchTerm }) {
const deferredSearchTerm = useDeferredValue(searchTerm);
const results = useMemo(() => {
// 重い検索処理
return performHeavySearch(deferredSearchTerm);
}, [deferredSearchTerm]);
return (
{/* 入力中は古い結果を表示、入力完了後に新しい結果 */}
{results.map(result => (
{result.title}
))}
);
}
// ✅ useTransitionで優先度の低い更新を管理
import { useTransition, useState } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('photos');
const selectTab = (nextTab) => {
startTransition(() => {
setTab(nextTab); // 優先度低い更新
});
};
return (
{isPending && Loading...}
);
}
パフォーマンスを考慮したuseEffectの最適化
依存配列の最適化
// ❌ 不要な依存関係
function BadDependencies({ user, settings }) {
const [profile, setProfile] = useState(null);
useEffect(() => {
if (user?.id) {
fetchProfile(user.id).then(setProfile);
}
}, [user, settings]); // settingsは不要な依存関係
}
// ✅ 必要最小限の依存関係
function GoodDependencies({ user, settings }) {
const [profile, setProfile] = useState(null);
useEffect(() => {
if (user?.id) {
fetchProfile(user.id).then(setProfile);
}
}, [user?.id]); // user.idのみ監視
}
useEffectEvent(実験的機能)の活用
// ✅ useEffectEventで非リアクティブなロジックを分離
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
// themeは最新の値を参照するが、依存配列には含めない
showNotification('Connected!', theme);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', onConnected);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // themeは依存配列に含めない
return Chat Room: {roomId};
}
AbortControllerでの非同期処理キャンセル
// ✅ AbortControllerで不要なリクエストをキャンセル
function UserSearch({ searchTerm }) {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (!searchTerm) {
setUsers([]);
return;
}
const abortController = new AbortController();
setIsLoading(true);
fetch(`/api/users?search=${searchTerm}`, {
signal: abortController.signal
})
.then(res => res.json())
.then(data => {
setUsers(data);
setIsLoading(false);
})
.catch(error => {
if (error.name !== 'AbortError') {
console.error('Search failed:', error);
setIsLoading(false);
}
});
// クリーンアップ関数で進行中のリクエストをキャンセル
return () => {
abortController.abort();
};
}, [searchTerm]);
return (
{isLoading && Searching...}
{users.map(user => (
{user.name}
))}
);
}
useEffectを適切に使うべき場面
useEffectが本当に必要な場面も理解しておきましょう:外部システムとの連携
// ✅ 外部ライブラリとの連携
function MapComponent({ location }) {
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
useEffect(() => {
// 外部ライブラリ(Google Maps等)の初期化
mapInstanceRef.current = new google.maps.Map(mapRef.current, {
center: location,
zoom: 13
});
return () => {
// クリーンアップ
mapInstanceRef.current = null;
};
}, []); // 初回のみ実行
useEffect(() => {
// locationが変更された時の処理
if (mapInstanceRef.current) {
mapInstanceRef.current.setCenter(location);
}
}, [location]);
return ;
}
DOM操作が必要な場合
// ✅ 直接的なDOM操作
function AutoFocusInput({ shouldFocus }) {
const inputRef = useRef(null);
useEffect(() => {
if (shouldFocus && inputRef.current) {
inputRef.current.focus();
inputRef.current.select(); // テキストを全選択
}
}, [shouldFocus]);
return ;
}
サブスクリプション管理
// ✅ イベントリスナーやサブスクリプション
function WindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 初回のみリスナー登録
return (
Window size: {windowSize.width} x {windowSize.height}
);
}
まとめ:useEffectとの正しい付き合い方
重要なポイント
- useEffectは最後の手段:まずは他の方法で解決できないか検討する
- 計算はレンダー時に行う:useMemoで最適化、useEffectは避ける
- データフェッチはライブラリを活用:React QueryやSWRが最適
- 状態の同期より派生状態:計算で求められるものはuseEffectを使わない
- 真の副作用のみでuseEffectを使用:DOM操作、外部システム連携、サブスクリプション
チェックリスト
useEffectを書く前に、以下をチェックしてください:- この処理はレンダー中に計算できませんか?
- derived stateで解決できませんか?
- useMemo/useCallbackで十分ではありませんか?
- イベントハンドラで直接実行できませんか?
- React QueryやSWRのようなライブラリが適切ではありませんか?

