use-effect-killer について
React コードを監査し、誤用または不要な `useEffect` を見つけて代替案を提示するスキルです。アンチパターン別に分類し、場所や修正案、件数を含む結果をまとめます。
主な機能
- 01React ソースの `useEffect` を検索
- 0213 種類のアンチパターンを分類
- 03ファイルパスと行番号を報告
- 04具体的なコードの代替案を提示
- 05妥当な Effect は対象外
活用シーン
- 01React コードの Effect 使用状況のレビュー
- 02Effect で保持される派生状態の発見
- 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
ワークフロー
-
すべての useEffect の使用箇所を見つけます。
$ARGUMENTS(パスが指定されていない場合はリポジトリ全体)を対象にuseEffectを検索します。.tsx、.ts、.jsx、.jsファイルに絞り込みます。 -
useEffect を含む各ファイルを読みます。 それぞれの useEffect 呼び出しを、以下のアンチパターン一覧に照らして分類します。コンポーネントにスコープされた DOM のクリーンアップ付きイベントリスナー、アニメーションのセットアップ、外部システムとの真の同期など、正当なエフェクトは対象外とします。
-
指摘事項ごとに、以下を記録します。
- ファイルパスと行番号
- 該当するアンチパターン
- 問題のあるコードスニペット
- 推奨される代替方法を使った、具体的な修正案
-
以下の出力形式で指摘事項を報告します。 アンチパターンごとにまとめ、件数を含む概要を記載します。
-
$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 を含むファイルを簡潔に列挙し、確認済みであることがユーザーにわかるようにしてください。