返回 Skill 列表
extension
分类: 开发与工程无需 API Key

react-anti-patterns

用于审查React代码——特别是AI生成的代码——以捕捉常见的反模式。涵盖了18种反模式及其检测难度,包括陈旧闭包、状态变异、useEffect滥用和布尔值爆炸。

person作者: jakexiaohubgithub

React Anti-patterns

Overview

18 anti-patterns commonly found in AI-generated and junior React code. Organized by detection difficulty — hard-to-detect bugs first.

Hard to Detect

1. Stale Closure

// ❌ 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);
};

The ref pattern is right here because the read happens in a UI event handler. When the stale callback lives inside an effect, React 19.2+ replaces this workaround with useEffectEvent — see react-useref, Case 3.

2. Component Inside Component

// ❌ 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. State Duplication (out of sync)

// ❌ 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;

Medium to Detect

4. State Mutation

// ❌ 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 Explosion

// ❌ 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. useCallback Without memo

// ❌ 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 Mirroring in 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. Mega-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. Granular useState Instead of useReducer

// ❌ 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 }));

Easy to Detect

10. useEffect for Derived State

// ❌ 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. Missing useEffect Cleanup

// ❌ 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} on Dynamic Lists

// ❌ 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} is OK only for static, never-reordered lists.

13. Manual Fetch Instead of React Query

// ❌ 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. Missing Loading/Error/Empty States

// ❌ 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. Conditional 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. && With Numbers Rendering "0"

// ❌ When count === 0, renders "0" in UI
{count && <CartBadge count={count} />}

// ✅ Explicit boolean
{count > 0 && <CartBadge count={count} />}

17. God Components

AI generates one 300-line component mixing fetch + logic + UI + forms. Split into focused components with their own state/hooks.

18. Batching Surprise (React 18+)

React 18 auto-batches all state updates (even in async). If you need immediate render (e.g., to measure DOM), use flushSync:

import { flushSync } from 'react-dom';
flushSync(() => setCount(1)); // renders immediately
console.log(ref.current.offsetHeight); // safe to measure

Code Review Checklist

| Check | Question | |-------|----------| | useEffect | Is it truly needed, or is this derived state? | | useEffect | Is there a cleanup return? | | fetch in effect | AbortController or ignore flag? | | Lists | key is stable ID, not index? | | useState + props | Props mirroring? | | useCallback/useMemo | Is there memo() on the receiving child? | | Server data | Should this be React Query instead? | | Component states | Loading, error, empty, success all handled? | | State shape | Boolean explosion? Use union type. | | Component definitions | Any components defined inside other components? | | Closures | Stale state in setTimeout/setInterval/async? |

References