useEffectを使いすぎてない?React副作用の正しい使い分けとモダンな代替手段

React開発で「なんとなくuseEffectを使っておけば動く」と思っていませんか?実は、useEffectは万能薬ではありません。むしろ、不適切な使い方をするとパフォーマンス問題や無限ループを引き起こす原因になります。

この記事では、useEffectの「使いすぎ」によくある問題から、正しい判断基準、そしてReact QueryやSWRなどモダンな代替手段まで、実践的に解説します。

あわせて読みたい
You Might Not Need an Effect – React The library for web and native user interfaces
目次

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}

{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のようなライブラリが適切ではありませんか?

今後のアクション

1. **既存コードの見直し**:不要なuseEffectがないかチェック 2. **React QueryやSWRの導入**:データフェッチングを効率化 3. **derived stateパターンの活用**:状態の同期を減らす 4. **useMemo/useCallbackの適切な使用**:パフォーマンスの最適化 useEffectは強力なツールですが、「何でもuseEffect」ではなく、**適材適所**で使い分けることが、保守性が高く高性能なReactアプリケーションを作る鍵です。

参考リンク

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次