react-anti-patterns について
react-anti-patterns は、AI 生成コードや初級者のコードでよく見られる React のアンチパターンを18種類まとめたコードレビュー用スキルです。検出難易度順の例とチェックリストを掲載しています。
主な機能
- 01React のアンチパターンを18種類収録
- 02検出難易度順に構成
- 03問題例と改善例を提示
- 04コードレビュー用チェックリストを収録
活用シーン
- 01AI 生成 React コードのレビュー
- 02古いクロージャや状態の不整合の調査
- 03コンポーネント、Hooks、リスト描画の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ b4r7x/ agent-skills --skill react-anti-patternsインストール後、Claude Code で/react-anti-patternsを入力して読み込まれたか確認します。
詳細
React のアンチパターン
概要
AI が生成したコードや、経験の浅い開発者の React コードでよく見られる 18 個のアンチパターンです。検出の難しさ順に、検出しにくいバグから並べています。
検出しにくいもの
1. 古いクロージャ
// ❌ count is always 0 in the timeout — closure captured old value
const handleDelayedAlert = () => {
setTimeout(() => alert(`Count: ${count}`), 3000);
};
// ✅ useRef for current value in async/timeout
const countRef = useRef(count);
useEffect(() => { countRef.current = count; });
const handleDelayedAlert = () => {
setTimeout(() => alert(`Count: ${countRef.current}`), 3000);
};
読み取りが UI イベントハンドラー内で行われるため、ここでは ref パターンが適切です。古いコールバックが effect 内にある場合、React 19.2 以降ではこの回避策の代わりに useEffectEvent を使用できます。react-useref の Case 3 を参照してください。
2. コンポーネント内に定義されたコンポーネント
// ❌ New component reference every render = unmount/remount cycle
function List({ items }) {
const ListItem = ({ item }) => <div>{item.name}</div>; // INSIDE!
return items.map(item => <ListItem key={item.id} item={item} />);
}
// ✅ Define outside
const ListItem = ({ item }) => <div>{item.name}</div>;
function List({ items }) {
return items.map(item => <ListItem key={item.id} item={item} />);
}
3. 状態の重複(同期ずれ)
// ❌ selectedUser is a copy — gets stale when users updates
const [users, setUsers] = useState([]);
const [selectedUser, setSelectedUser] = useState(null); // full object!
// ✅ Store only ID — derive the object
const [selectedUserId, setSelectedUserId] = useState(null);
const selectedUser = users.find(u => u.id === selectedUserId) ?? null;
検出が中程度のもの
4. 状態の変更
// ❌ Mutating — React doesn't see the change
const todo = todos.find(t => t.id === id);
todo.done = !todo.done; // mutation!
setTodos(todos); // same reference — no re-render
// ✅ New reference
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
5. Boolean の爆発
// ❌ 2^4 = 16 combinations, most impossible
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isRetrying, setIsRetrying] = useState(false);
// ✅ Finite state machine
type Status = 'idle' | 'loading' | 'success' | 'error' | 'retrying';
const [status, setStatus] = useState<Status>('idle');
6. memo なしの useCallback
// ❌ useCallback alone = dead code (Child re-renders anyway)
const handleClick = useCallback(() => {}, []);
return <Child onClick={handleClick} />;
// ✅ Only useful WITH memo on the child
const Child = memo(function Child({ onClick }) { /* ... */ });
7. props の state へのコピー
// ❌ Syncing props to state via useEffect
const [title, setTitle] = useState(initialTitle);
useEffect(() => { setTitle(initialTitle); }, [initialTitle]);
// ✅ Fully controlled
<input value={title} onChange={e => onChange(e.target.value)} />
// ✅ Or fully uncontrolled with key reset
<EditableTitle key={userId} initialTitle={title} />
8. 巨大な Context
// ❌ One context — any change re-renders ALL consumers
<AppContext.Provider value={{ user, cart, theme, notifications }}>
// ✅ Separate, isolated contexts
<ThemeProvider><AuthProvider><CartProvider>{children}</CartProvider></AuthProvider></ThemeProvider>
9. useReducer の代わりに細分化した useState を使う
// ❌ 6 related useState calls — hard to reset, easy to desync
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ... 4 more
// ✅ One state object or useReducer
const [form, setForm] = useState({ firstName: '', lastName: '', /* ... */ });
const updateField = (field) => (e) => setForm(prev => ({ ...prev, [field]: e.target.value }));
検出しやすいもの
10. 派生状態に useEffect を使う
// ❌ Two unnecessary renders
useEffect(() => { setTotal(items.reduce((s, i) => s + i.price, 0)); }, [items]);
// ✅ Compute during render
const total = items.reduce((s, i) => s + i.price, 0);
11. useEffect のクリーンアップ漏れ
// ❌ Memory leak
useEffect(() => { setInterval(() => setCount(c => c + 1), 1000); }, []);
// ✅ Always cleanup
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);
12. 動的リストでの key={index}
// ❌ Removing item from middle = broken re-renders, lost input state
{items.map((item, index) => <TodoItem key={index} item={item} />)}
// ✅ Stable unique ID
{items.map(item => <TodoItem key={item.id} item={item} />)}
key={index} は、並べ替えが発生しない静的なリストでのみ問題ありません。
13. React Query の代わりに手動で fetch する
// ❌ No cache, no retry, no dedup, race conditions
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => { fetch(`/api/users/${id}`).then(/* ... */); }, [id]);
// ✅ React Query handles all edge cases
const { data: user, isLoading } = useQuery({
queryKey: ['user', id],
queryFn: () => fetch(`/api/users/${id}`).then(r => r.json()),
});
14. ローディング/エラー/空状態の処理漏れ
// ❌ Only happy path — crashes on undefined
return data.map(p => <ProductCard key={p.id} product={p} />);
// ✅ All states handled
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
if (!data?.length) return <EmptyState />;
return data.map(p => <ProductCard key={p.id} product={p} />);
15. 条件付き Hooks
// ❌ Breaks Rules of Hooks — React loses track of hook order
if (isAdmin) { const data = useAdminData(); }
// ✅ All hooks at top, conditions in JSX or inside hooks
const adminData = useAdminData();
if (!userId) return null; // return AFTER all hooks
16. 数値に && を使うと「0」が表示される
// ❌ When count === 0, renders "0" in UI
{count && <CartBadge count={count} />}
// ✅ Explicit boolean
{count > 0 && <CartBadge count={count} />}
17. 巨大なコンポーネント
AI は fetch、ロジック、UI、フォームを混在させた 300 行のコンポーネントを生成します。それぞれに専用の state/hooks を持つ、役割を絞ったコンポーネントに分割してください。
18. バッチ処理による予期しない動作(React 18 以降)
React 18 は、非同期処理内も含めてすべての状態更新を自動的にバッチ処理します。即座にレンダリングする必要がある場合(たとえば、DOM を測定する場合)は、flushSync を使います。
import { flushSync } from 'react-dom';
flushSync(() => setCount(1)); // renders immediately
console.log(ref.current.offsetHeight); // safe to measure
コードレビューのチェックリスト
| 確認項目 | 質問 |
|---|---|
useEffect | 本当に必要ですか。それとも派生状態ですか? |
useEffect | クリーンアップの return はありますか? |
effect 内の fetch | AbortController または無視フラグを使っていますか? |
| リスト | key は index ではなく、安定した ID ですか? |
useState と props | props をコピーしていませんか? |
useCallback/useMemo | 呼び出し先の子コンポーネントに memo() はありますか? |
| サーバーデータ | React Query を使うべきではありませんか? |
| コンポーネントの状態 | ローディング、エラー、空、成功のすべてを処理していますか? |
| 状態の形 | Boolean が増えすぎていませんか?ユニオン型を使いましょう。 |
| コンポーネントの定義 | 別のコンポーネント内に定義されたコンポーネントはありませんか? |
| クロージャ | setTimeout/setInterval/async 内で古い状態を参照していませんか? |
参考資料
- You Might Not Need an Effect — React docs — useEffect に関する最も一般的なアンチパターンを特定する公式ガイド
- React Anti-Patterns and Best Practices — 実例を交えた、推奨事項と禁止事項の包括的な一覧
コメント
Markdown 対応