mcpField
R
react-anti-patterns

react-anti-patterns

github.com/b4r7x测试与质量038 次安装12 周前更新
代码仓库

关于 react-anti-patterns

react-anti-patterns 是一份 React 代码审查技能,整理了 AI 生成及初级代码中常见的 18 种反模式。内容按检测难度排列,并附有示例和审查清单。

主要功能

  1. 01汇总 18 种 React 反模式
  2. 02按检测难度组织内容
  3. 03提供问题代码与改进示例
  4. 04附带代码审查清单

适用场景

  1. 01审查 AI 生成的 React 代码
  2. 02排查状态不同步或闭包过期问题
  3. 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);
};

这里使用 ref 模式是正确的,因为读取操作发生在 UI 事件处理程序中。当旧回调位于 effect 内时,React 19.2+ 可以用 useEffectEvent 替代这种变通方法——参见 react-useref 的案例 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. 布尔状态过多
// ❌ 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 却没有 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 镜像到状态中
// ❌ 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. 使用多个细粒度的 useState,而不是 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 }));

容易检测

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
// ❌ 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. 条件式调用 Hook
// ❌ 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 会生成一个 300 行的组件,把数据获取、逻辑、UI 和表单混在一起。拆分成职责明确且拥有各自状态和 Hook 的组件。

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 是否为稳定 ID,而不是索引?
useState + props是否将 props 镜像到了状态中?
useCallback/useMemo接收它的子组件是否使用了 memo()?
服务端数据是否应该改用 React Query?
组件状态是否处理了加载、错误、空和成功状态?
状态结构是否存在布尔状态过多的问题?应使用联合类型。
组件定义是否有组件定义在其他组件内部?
闭包setTimeout/setInterval/异步操作中的状态是否已过期?

参考资料

评论

支持 Markdown