mcpField
D
dont-use-use-effect

dont-use-use-effect

github.com/jonmumm開発367 回インストール3 日前に更新
リポジトリ

dont-use-use-effect について

Don't Use useEffectは、不要なEffectを別の方法に置き換えるためのReact開発者向けスキルです。派生状態、イベント処理、データ取得、購読、プロパティ変更時のリセットを扱い、Effectが適切な場面も示します。

主な機能

  1. 01プロパティや状態から値を導出
  2. 02イベント処理をハンドラーへ移動
  3. 03データ取得の代替手段を利用
  4. 04useSyncExternalStoreで購読
  5. 05keyで状態をリセット

活用シーン

  1. 01冗長な状態更新Effectの確認
  2. 02Reactのデータ取得方法の選択
  3. 03外部ストアの手動購読の置き換え

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jonmumm/skills --skill dont-use-use-effect

インストール後、Claude Code で/dont-use-use-effectを入力して読み込まれたか確認します。

詳細

useEffect を使わない

useEffect は、React で最も誤用されているフックの 1 つです。開発者がこれを使おうとするケースの大半では、よりシンプルで、パフォーマンスに優れ、適切な代替手段があります。不要な useEffect を使うたびに、余分なレンダーサイクルが発生し、バグ(古いクロージャー、競合状態、無限ループ)のリスクが高まり、コンポーネントの理解も難しくなります。

基本原則: Effect は外部システムとの同期(DOM API、タイマー、WebSocket、サードパーティ製ウィジェット)に使います。行おうとしている処理がユーザーイベントへの応答である場合や、既存の state/props から計算できる場合、Effect は必要ありません。


1. 派生 state — 計算するだけにする

最もよくある useEffect のアンチパターンは、ほかの state や props から計算できる値を state に保存することです。

// 🔴 BAD: Redundant state + unnecessary Effect
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  const [fullName, setFullName] = useState('');

  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);

  return <span>{fullName}</span>;
}

firstName または lastName が変わるたびに、2 回のレンダーが発生します。1 回目は古い fullName を使い、もう 1 回は Effect が値を更新した後です。

// ✅ GOOD: Calculate during render
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // Derived — no state, no Effect, one render
  const fullName = firstName + ' ' + lastName;

  return <span>{fullName}</span>;
}

時間のかかる計算には、state に保存する代わりに useMemo でラップします。

// ✅ GOOD: Memoize expensive derivations
function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');

  // Only recomputes when todos or filter change, not when newTodo changes
  const visibleTodos = useMemo(
    () => getFilteredTodos(todos, filter),
    [todos, filter]
  );

  return <ul>{visibleTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul>;
}

2. イベント駆動のロジック — イベントハンドラーを使う

コードを実行するきっかけがユーザーの操作(クリック、送信、入力)である場合、そのコードは Effect ではなくイベントハンドラーに記述します。Effect が実行されるのは、ユーザーが操作したときではなく、コンポーネントがレンダーされたときです。

// 🔴 BAD: Event-specific logic triggered by state change in an Effect
function ProductPage({ product, addToCart }) {
  useEffect(() => {
    if (product.isInCart) {
      showNotification(`Added ${product.name} to the cart!`);
    }
  }, [product]);

  function handleBuyClick() {
    addToCart(product);
  }
  // ...
}

このコードにはバグがあります。商品がすでにカートに入っている場合、ページを再読み込みすると通知が表示されます。

// ✅ GOOD: Side effects belong in the event handler that caused them
function ProductPage({ product, addToCart }) {
  function buyProduct() {
    addToCart(product);
    showNotification(`Added ${product.name} to the cart!`);
  }

  function handleBuyClick() {
    buyProduct();
  }

  function handleCheckoutClick() {
    buyProduct();
    navigateTo('/checkout');
  }
  // ...
}

目安: コードを実行すべきユーザー操作を具体的に特定できる場合は、その操作のイベントハンドラーに記述します。


3. Effect の連鎖 — イベントハンドラーにまとめる

ほかの state に基づいて state を設定する Effect を連鎖させると、不要な再レンダーが次々に発生し、変更に弱く脆いコードになります。

// 🔴 BAD: Chain of Effects triggering each other (4 render passes!)
function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);
  const [isGameOver, setIsGameOver] = useState(false);

  useEffect(() => {
    if (card !== null && card.gold) {
      setGoldCardCount(c => c + 1);
    }
  }, [card]);

  useEffect(() => {
    if (goldCardCount > 3) {
      setRound(r => r + 1);
      setGoldCardCount(0);
    }
  }, [goldCardCount]);

  useEffect(() => {
    if (round > 5) setIsGameOver(true);
  }, [round]);
  // ...
}
// ✅ GOOD: Derive what you can, compute the rest in the event handler
function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // Derived — no state needed
  const isGameOver = round > 5;

  function handlePlaceCard(nextCard) {
    if (isGameOver) throw Error('Game already ended.');

    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount < 3) {
        setGoldCardCount(goldCardCount + 1);
      } else {
        setGoldCardCount(0);
        setRound(round + 1);
        if (round === 5) alert('Good game!');
      }
    }
  }
  // ...
}

4. データ取得 — データ取得ライブラリを使う

useEffect でデータを取得するのは脆弱です。競合状態、読み込み状態、キャッシュ、エラー処理、クリーンアップをすべて手動で処理する必要があります。useEffect による直接的なデータ取得には、キャッシュ、重複排除、SSR のサポートがありません。

// 🔴 BAD: Manual fetch in useEffect — race conditions, no caching, no SSR
function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    let ignore = false;
    setIsLoading(true);
    fetchResults(query).then(json => {
      if (!ignore) {
        setResults(json);
        setIsLoading(false);
      }
    });
    return () => { ignore = true; };
  }, [query]);
  // ...
}
// ✅ GOOD: Use TanStack Query (or your framework's data primitive)
import { useQuery } from '@tanstack/react-query';

function SearchResults({ query }) {
  const { data: results = [], isLoading } = useQuery({
    queryKey: ['search', query],
    queryFn: () => fetchResults(query),
    enabled: !!query,
  });
  // Automatic caching, deduplication, race-condition handling,
  // background refetching, and SSR support — for free.
}

そのほかの有効な代替手段:

  • Next.js / Remix: loader 関数、Server Components、use() + Suspense
  • SWR: useSWR(key, fetcher) — TanStack Query と同様です
  • tRPC: React Query との統合が組み込まれた、エンドツーエンドの型安全なデータ取得

フレームワークもライブラリもなく、どうしても useEffect でデータを取得する必要がある場合は、最低限、次の対応を行います。

  1. 競合状態を防ぐため、クリーンアップ用の ignore フラグを追加する
  2. ロジックを再利用可能なカスタムフック(例:useData(url))に切り出す
  3. 読み込み中、エラー、データなしの各状態を明示的に処理する

5. 外部ストアの購読 — useSyncExternalStore を使う

useEffect と setState を使って外部データソース(ブラウザー API、サードパーティ製ストア、Observable)を購読する方法は、定型コードが多くなるうえ、並行モードで tearing を引き起こす可能性があります。

// 🔴 BAD: Manual subscription with useEffect
function OnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return <span>{isOnline ? '✅ Online' : '❌ Offline'}</span>;
}
// ✅ GOOD: useSyncExternalStore — concurrent-safe, less code
import { useSyncExternalStore } from 'react';

function subscribe(callback: () => void) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function getSnapshot() {
  return navigator.onLine;
}

function OnlineStatus() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  return <span>{isOnline ? '✅ Online' : '❌ Offline'}</span>;
}

状態管理ライブラリを使う場合は、手動で購読する代わりに、ライブラリに組み込まれている React フックを使います。XState、Zustand、Redux、Nanostores でセレクターを使うパターンについては、react-render-performance スキルを参照してください。


6. props の変更時に state をリセットする — key を使う

props が変わったときに state をリセットするために useEffect を使わないでください。React には key prop という組み込みの仕組みがあります。

// 🔴 BAD: Resetting state in an Effect
function EditProfile({ userId }) {
  const [comment, setComment] = useState('');

  useEffect(() => {
    setComment('');
  }, [userId]);

  return <textarea value={comment} onChange={e => setComment(e.target.value)} />;
}
// ✅ GOOD: Use key to reset component state
function ProfilePage({ userId }) {
  // When userId changes, React unmounts the old EditProfile and mounts a new one
  return <EditProfile userId={userId} key={userId} />;
}

function EditProfile({ userId }) {
  const [comment, setComment] = useState('');
  return <textarea value={comment} onChange={e => setComment(e.target.value)} />;
}

useEffect が適切な場合

エフェクトは、コンポーネントをReact の外部にあるものと同期する場合に適しています。

使用例適切な理由
React 以外のウィジェット(地図、チャート、動画プレーヤー)の統合React の状態 → 命令型 DOM API の同期
WebSocket または EventSource 接続のセットアップコンポーネントに紐づく外部システムのライフサイクル
DOM レイアウトの測定(getBoundingClientRect、ResizeObserver)レンダリング後の DOM 情報の読み取り
フォーカスやスクロール位置の命令型管理命令型 DOM 操作
ハードウェア API(カメラ、位置情報)への接続購読とクリーンアップが必要な外部システム
document.title などのグローバルな副作用React の状態 → ブラウザー API の同期

これらの場合でも、ライブラリがすでに処理してくれるかどうかを検討してください(例:react-intersection-observer、framer-motion)。


判断フローチャート

useEffect を書く前に、次の質問を自分に問いかけてください。

  1. 既存の props/state からこれを計算できますか? → レンダリング中に導出します(または useMemo を使います)。
  2. ユーザー操作への応答ですか? → イベントハンドラーに記述します。
  3. データを取得していますか? → TanStack Query、SWR、またはフレームワークのデータプリミティブを使います。
  4. 外部ストアを購読していますか? → useSyncExternalStore またはライブラリのフックを使います。
  5. prop が変わったときに state をリセットしていますか? → key prop を使います。
  6. React が制御しない外部システムと同期していますか? → ✅ これは有効な useEffect の使い方です。

まとめのチェックリスト

  • 他の state/props から導出される state を設定する useEffect は使わず、代わりにレンダリング中に計算します
  • ユーザーイベントへの応答としてロジックを実行する useEffect は使わず、代わりにイベントハンドラーを使います
  • state の更新を通じて互いをトリガーする useEffect の連鎖は作らず、イベントハンドラーまたは導出値にまとめます
  • データ取得に生の useEffect は使わず、TanStack Query、SWR、またはフレームワークのデータプリミティブを使います
  • 外部ストアの購読に useEffect + setState は使わず、useSyncExternalStore またはライブラリのフックを使います
  • props が変わったときの state のリセットに useEffect は使わず、key prop を使います
  • 残っているすべての useEffect が、本物の外部システム(DOM API、WebSocket、サードパーティ製ウィジェット)との同期に使われています

コメント