mcpField
U
use-effect-killer

use-effect-killer

github.com/victor36maxテスト・品質1070 回インストール29 週間前に更新
リポジトリ

use-effect-killer について

React コードを監査し、誤用または不要な `useEffect` を見つけて代替案を提示するスキルです。アンチパターン別に分類し、場所や修正案、件数を含む結果をまとめます。

主な機能

  1. 01React ソースの `useEffect` を検索
  2. 0213 種類のアンチパターンを分類
  3. 03ファイルパスと行番号を報告
  4. 04具体的なコードの代替案を提示
  5. 05妥当な Effect は対象外

活用シーン

  1. 01React コードの Effect 使用状況のレビュー
  2. 02Effect で保持される派生状態の発見
  3. 03データ取得時の古い応答への対処確認

インストール

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

$npx skills add https://github.com/victor36max/use-effect-killer --skill use-effect-killer

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

同梱ファイルLICENSEREADME.md

詳細

useEffect キラー

React コードベースを監査し、不要または誤用されている useEffect 呼び出しを特定して、慣用的な代替方法を提案します。

参考:https://react.dev/learn/you-might-not-need-an-effect

ワークフロー

  1. すべての useEffect の使用箇所を見つけます。 $ARGUMENTS(パスが指定されていない場合はリポジトリ全体)を対象に useEffect を検索します。.tsx、.ts、.jsx、.js ファイルに絞り込みます。

  2. useEffect を含む各ファイルを読みます。 それぞれの useEffect 呼び出しを、以下のアンチパターン一覧に照らして分類します。コンポーネントにスコープされた DOM のクリーンアップ付きイベントリスナー、アニメーションのセットアップ、外部システムとの真の同期など、正当なエフェクトは対象外とします。

  3. 指摘事項ごとに、以下を記録します。

    • ファイルパスと行番号
    • 該当するアンチパターン
    • 問題のあるコードスニペット
    • 推奨される代替方法を使った、具体的な修正案
  4. 以下の出力形式で指摘事項を報告します。 アンチパターンごとにまとめ、件数を含む概要を記載します。

  5. $ARGUMENTS が空の場合、プロジェクト内のすべての React コンポーネントファイルをスキャンします。

アンチパターン一覧

以下のパターンを使って、検出した各 useEffect を分類してください。

1. 派生状態

他の状態や props から計算できる値を、useEffect で更新する状態。

検出方法: useEffect の本体でセッターを呼び出しており、設定される値が、レンダー中にすでに利用可能な状態や props の純粋関数として表現できる。

悪い例:

const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

修正方法: 余分な状態とエフェクトを削除します。インラインで計算します。

const fullName = firstName + ' ' + lastName;
2. コストの高い派生計算

#1 と同様ですが、計算コストが高いため、開発者が「キャッシュ」する目的で useEffect と状態を使っているパターンです。

検出方法: props や状態のデータを変換またはフィルタリングし、別の状態変数に格納する useEffect。.filter()、.map()、.sort()、.reduce() などがよく使われます。

悪い例:

const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
  setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);

修正方法: useMemo を使います。

const visibleTodos = useMemo(
  () => getFilteredTodos(todos, filter),
  [todos, filter]
);
3. props の変更時にすべての状態をリセットする

ID を表す props(例:userId、itemId)が変わったとき、useEffect を使って状態変数をリセットするパターンです。

検出方法: 本体で1つ以上の状態変数を初期値に設定し、依存配列に prop が含まれている useEffect。

悪い例:

function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');
  useEffect(() => {
    setComment('');
  }, [userId]);

修正方法: 子コンポーネントに切り出し、key を使ってリセットします。

function ProfilePage({ userId }) {
  return <Profile userId={userId} key={userId} />;
}
function Profile({ userId }) {
  const [comment, setComment] = useState('');
4. props の変更時に一部の状態を調整する

状態を完全にはリセットせず、props の変更時に useEffect で一部を更新するパターンです。

検出方法: prop への依存があり、条件付きで状態を更新する useEffect。選択項目を null にする、またはサブフィールドをリセットするケースがよくあります。

悪い例:

function List({ items }) {
  const [selection, setSelection] = useState(null);
  useEffect(() => {
    setSelection(null);
  }, [items]);

修正方法(推奨): 値を状態として保存せず、レンダー中に導出します。

function List({ items }) {
  const [selectedId, setSelectedId] = useState(null);
  const selection = items.find(item => item.id === selectedId) ?? null;

修正方法(代替案): 前回の props を保存し、レンダー中に調整します。注意:items !== prevItems のガードは必須です。これがないと、無限レンダーループが発生します。このパターンでは再レンダーも1回余分に発生します。

const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
  setPrevItems(items);
  setSelection(null);
}
5. イベント処理をエフェクト内に記述する

ユーザー操作に対するトースト、通知、ナビゲーション、分析などの副作用を、それを引き起こしたイベントハンドラーではなく useEffect に記述するパターンです。

検出方法: イベントハンドラーで設定された状態変数をきっかけに、showNotification、toast、navigate、router.push、alert のような関数を呼び出したり、分析ログを記録したりする useEffect。

悪い例:

useEffect(() => {
  if (product.isInCart) {
    showNotification(`Added ${product.name} to cart!`);
  }
}, [product]);

修正方法: イベントハンドラー内に移動します。

function handleBuyClick() {
  addToCart(product);
  showNotification(`Added ${product.name} to cart!`);
}
6. useEffect 内での POST/ミューテーション

状態変数をトリガーとして、useEffect からネットワークリクエストを実行するパターンです。

検出方法: イベントハンドラーで設定された状態変数(「送信ペイロード」状態などがよく使われます)に依存し、fetch、post、axios、mutate、useMutation、tRPC のミューテーションなどを呼び出す useEffect。

悪い例:

const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
  if (jsonToSubmit !== null) {
    post('/api/register', jsonToSubmit);
  }
}, [jsonToSubmit]);

修正方法: イベントハンドラーから直接呼び出します。

function handleSubmit(e) {
  e.preventDefault();
  post('/api/register', { firstName, lastName });
}
7. Effect チェーン

複数の useEffect があり、そのうちの 1 つが設定した state によって次の useEffect がトリガーされ、連鎖を形成します。

検出方法: 同じコンポーネント内に 2 つ以上の useEffect 呼び出しがあり、一方の依存配列に、もう一方の中で設定される state 変数が含まれているかを確認します。連続して state を設定するパターンを探します。

問題のある例:

useEffect(() => { if (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]);

修正方法: 導出できる値は導出し(const isGameOver = round > 5)、残りはイベントハンドラーにまとめます。

const isGameOver = round > 5;

function handlePlaceCard(nextCard) {
  setCard(nextCard);
  if (nextCard.gold) {
    if (goldCardCount < 3) {
      setGoldCardCount(goldCardCount + 1);
    } else {
      setGoldCardCount(0);
      setRound(round + 1);
    }
  }
}
8. Effect 内でのアプリの初期化

認証チェック、localStorage の読み取り、設定の読み込みなどの一度限りのセットアップコードを useEffect([], ...) 内で実行すると、Strict Mode の二重マウントで問題が発生します。

検出方法: [] の依存配列を持つ useEffect が、認証トークンの確認、localStorage の読み取り、グローバル設定など、初期化処理を実行しているかを確認します。特に、二度実行すべきでない副作用を伴う処理は問題になります。

問題のある例:

useEffect(() => {
  loadDataFromLocalStorage();
  checkAuthToken();
}, []);

修正方法: モジュールレベルのフラグでガードし、Strict Mode の二重マウント時でも一度だけ実行されるようにします。

let didInit = false;

function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true;
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
}

本当に副作用がなく、React のライフサイクルも必要としないコードは、モジュールレベルで実行しても問題ありません。

if (typeof window !== 'undefined') {
  // Only for pure reads — not for auth tokens or anything with side effects
  const cachedTheme = localStorage.getItem('theme');
}
9. Effect 経由で親に通知する

state を更新した後に useEffect 内で親のコールバック(onChange など)を呼び出すのではなく、state の更新と同時に呼び出します。

検出方法: 本文内で現在の state 値を渡して、プロパティのコールバック(onChange、onUpdate、onSelect など)を呼び出す useEffect を探します。

問題のある例:

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);
  useEffect(() => {
    onChange(isOn);
  }, [isOn, onChange]);

修正方法: イベントハンドラー内でコールバックを呼び出します。

function handleClick() {
  const nextIsOn = !isOn;
  setIsOn(nextIsOn);
  onChange(nextIsOn);
}

または、コンポーネントを完全に制御された状態にし(ローカル state を削除して、親に管理させます)。

10. Effect 経由で親にデータを渡す

子コンポーネントがデータを取得または計算し、Effect コールバックを介して親に渡します。

検出方法: 子コンポーネント内のフックまたは fetch で取得したデータを使って、useEffect から onFetched、onData、onLoaded などのプロパティのコールバックを呼び出していないかを確認します。

問題のある例:

function Child({ onFetched }) {
  const data = useSomeAPI();
  useEffect(() => {
    if (data) onFetched(data);
  }, [onFetched, data]);

修正方法: データ取得を親に移します。データは親から子へ渡し、子から親へ渡さないようにします。

function Parent() {
  const data = useSomeAPI();
  return <Child data={data} />;
}
11. 外部ストアの購読

ブラウザー API や外部ストアとの同期のために、useEffect 内で addEventListener/removeEventListener または購読処理を手動で実行します。

検出方法: コンポーネントの ref ではなく、window、document、navigator に対する addEventListener/removeEventListener を使い、外部 state を React の state に同期する useEffect を探します。また、グローバルストアに対する .subscribe()/.unsubscribe() のパターンも確認します。適切なクリーンアップを伴う ref スコープの DOM リスナーは正当なものなので、警告しないでください。

問題のある例:

useEffect(() => {
  function update() { setIsOnline(navigator.onLine); }
  window.addEventListener('online', update);
  window.addEventListener('offline', update);
  return () => {
    window.removeEventListener('online', update);
    window.removeEventListener('offline', update);
  };
}, []);

修正方法: useSyncExternalStore を使用します。

const isOnline = useSyncExternalStore(
  (cb) => {
    window.addEventListener('online', cb);
    window.addEventListener('offline', cb);
    return () => {
      window.removeEventListener('online', cb);
      window.removeEventListener('offline', cb);
    };
  },
  () => navigator.onLine,
  () => true
);
12. Effect 経由で props から state を初期化する

初回レンダー時に props から state を設定するために useEffect を使うのではなく、props を直接 useState に渡します。

検出方法: [] の依存配列を持つ useEffect が setter に prop 値を渡しているか、または state の初期値(null など)が異なり、すぐに上書きされる [prop] の useEffect がないかを確認します。

問題のある例:

function Editor({ initialContent }) {
  const [content, setContent] = useState(null);
  useEffect(() => {
    setContent(initialContent);
  }, []);
}

修正方法: prop を初期 state 値として直接渡します。

function Editor({ initialContent }) {
  const [content, setContent] = useState(initialContent);
}

これにより、コンポーネントが最初に null でレンダーされ、その直後に prop の値で再レンダーされるときのちらつきを解消できます。

13. クリーンアップなしの fetch

古いレスポンス(競合状態)を処理せずに、useEffect 内でデータを取得します。

検出方法: fetch、axios、または非同期呼び出しを含み、完了時に state を設定する useEffect を探します。ただし、ignore フラグを設定するか、AbortController.abort() を呼び出すクリーンアップ関数がある場合は除きます。

問題のある例:

useEffect(() => {
  fetchResults(query).then(json => {
    setResults(json);
  });
}, [query]);

修正方法: ignore フラグまたは abort コントローラーを追加します。より良い方法は、データ取得ライブラリ(TanStack Query、SWR など)を使用することです。

useEffect(() => {
  let ignore = false;
  fetchResults(query).then(json => {
    if (!ignore) setResults(json);
  });
  return () => { ignore = true; };
}, [query]);

正当な useEffect の使用例(報告しない)

以下は有効な useEffect の使用例です。対象から除外してください。

  • 適切なクリーンアップを伴う、ref スコープの DOM イベントの購読(例:特定の要素 ref に対する ResizeObserver、ref に対する IntersectionObserver)。注:レンダリング状態を更新する window/document レベルの購読には、代わりに useSyncExternalStore を使用してください(#11 を参照)。
  • コンポーネントのマウント中にスコープを限定してアニメーションやタイマーを実行する
  • 真に外部のシステムとの同期(WebSocket 接続、サードパーティーウィジェットの初期化)
  • コンポーネントの表示時に実行すべき分析/ログ記録(ユーザー操作時ではない)
  • マウント時のフォーカス管理

出力形式

次の形式でレポートを作成してください。

## useEffect 監査結果

### 概要
- **スキャンしたファイル数:** N
- **見つかった useEffect の数:** N
- **検出したアンチパターンの数:** N
- **正当な使用例(対象外):** N

### 指摘事項

#### 1. 派生状態(X 件)

**`src/components/UserProfile.tsx:42`**
(現在のコードを表示)
**問題:** fullName は first と last から導出されるため、effect は不要です。
**修正:** `const fullName = first + ' ' + last;` とし、`fullName` の state を削除します。

---
(アンチパターンごとに分類し、指摘事項ごとに繰り返す)

### 問題のないファイル
レビューに合格した useEffect を含むファイルを簡潔に列挙し、確認済みであることがユーザーにわかるようにしてください。

コメント