dont-use-use-effect について
Don't Use useEffectは、不要なEffectを別の方法に置き換えるためのReact開発者向けスキルです。派生状態、イベント処理、データ取得、購読、プロパティ変更時のリセットを扱い、Effectが適切な場面も示します。
主な機能
- 01プロパティや状態から値を導出
- 02イベント処理をハンドラーへ移動
- 03データ取得の代替手段を利用
- 04useSyncExternalStoreで購読
- 05keyで状態をリセット
活用シーン
- 01冗長な状態更新Effectの確認
- 02Reactのデータ取得方法の選択
- 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 でデータを取得する必要がある場合は、最低限、次の対応を行います。
- 競合状態を防ぐため、クリーンアップ用の
ignoreフラグを追加する - ロジックを再利用可能なカスタムフック(例:
useData(url))に切り出す - 読み込み中、エラー、データなしの各状態を明示的に処理する
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 を書く前に、次の質問を自分に問いかけてください。
- 既存の props/state からこれを計算できますか? → レンダリング中に導出します(または
useMemoを使います)。 - ユーザー操作への応答ですか? → イベントハンドラーに記述します。
- データを取得していますか? → TanStack Query、SWR、またはフレームワークのデータプリミティブを使います。
- 外部ストアを購読していますか? →
useSyncExternalStoreまたはライブラリのフックを使います。 - prop が変わったときに state をリセットしていますか? →
keyprop を使います。 - React が制御しない外部システムと同期していますか? → ✅ これは有効な
useEffectの使い方です。
まとめのチェックリスト
- 他の state/props から導出される state を設定する
useEffectは使わず、代わりにレンダリング中に計算します - ユーザーイベントへの応答としてロジックを実行する
useEffectは使わず、代わりにイベントハンドラーを使います - state の更新を通じて互いをトリガーする
useEffectの連鎖は作らず、イベントハンドラーまたは導出値にまとめます - データ取得に生の
useEffectは使わず、TanStack Query、SWR、またはフレームワークのデータプリミティブを使います - 外部ストアの購読に
useEffect+setStateは使わず、useSyncExternalStoreまたはライブラリのフックを使います - props が変わったときの state のリセットに
useEffectは使わず、keyprop を使います - 残っているすべての
useEffectが、本物の外部システム(DOM API、WebSocket、サードパーティ製ウィジェット)との同期に使われています