在 React 开发中,写出“能跑”的代码很容易,但写出“高效、可维护、无隐患”的代码却需要避开无数陷阱。许多看似合理的写法,实际上隐藏着严重的性能问题或逻辑 Bug。
本文整理了 React 开发中最常见的反模式(Anti-Patterns) ,并提供相应的重构方案,帮助你打造健壮的代码库。
一、性能杀手类反模式
1.1 在 JSX 中直接创建对象/函数
❌ 反模式:
function ListItem({ item }) {
// 每次渲染都会创建新的对象和函数引用
const style = { color: 'red' };
const handleClick = () => console.log(item.id);
return <div style={style} onClick={handleClick}>{item.name}</div>;
}
后果:即使 item 没变,style 和 handleClick 的引用也变了。如果 ListItem 被 React.memo 包裹,它将永远失效,导致子组件无限重渲染。
✅ 修正:
function ListItem({ item }) {
const handleClick = useCallback(() => console.log(item.id), [item.id]);
// 样式尽量提取到 CSS 文件或 styled-components,或使用 useMemo
return <div className="text-red" onClick={handleClick}>{item.name}</div>;
}
1.2 滥用 useEffect 进行派生状态计算
❌ 反模式:
function Cart({ items }) {
const [total, setTotal] = useState(0);
useEffect(() => {
// 每次 items 变化都触发 Effect,多余且易出错
const newTotal = items.reduce((sum, i) => sum + i.price, 0);
setTotal(newTotal);
}, [items]);
return <div>Total: {total}</div>;
}
后果:增加了不必要的渲染周期(Render -> Effect -> SetState -> Re-render)。
✅ 修正:
function Cart({ items }) {
// 渲染期间直接计算,React 会缓存结果
const total = items.reduce((sum, i) => sum + i.price, 0);
return <div>Total: {total}</div>;
}
注:只有在计算极其耗时(如大数据排序)时,才考虑 useMemo。
二、逻辑陷阱类反模式
2.1 条件调用 Hooks
❌ 反模式:
function UserComponent({ isAdmin }) {
if (isAdmin) {
useEffect(() => { /* 监控管理员操作 */ });
}
// ...
}
后果:违反 Rules of Hooks。Hooks 的调用顺序必须一致,否则会导致状态错位(State Mismatch),引发难以调试的 Bug。
✅ 修正:
function UserComponent({ isAdmin }) {
useEffect(() => {
if (!isAdmin) return;
/* 监控管理员操作 */
}, [isAdmin]);
}
2.2 索引(Index)作为 Key
❌ 反模式:
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
))}
后果:当列表发生排序、删除或插入时,React 会错误地复用组件实例,导致输入框内容错乱、状态丢失。
✅ 修正:
始终使用业务唯一 ID:key={todo.id}。如果没有唯一 ID,考虑在数据结构生成时就赋予 UUID。
2.3 在 useEffect 中遗漏依赖项
❌ 反模式:
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// 这里的 count 永远是初始值 0 (闭包陷阱)
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []); // 依赖项为空
}
后果:状态不更新或逻辑错误。
✅ 修正:
- 方案 A:使用函数式更新
setCount(c => c + 1)。 - 方案 B:正确填写依赖项
[count](需注意可能导致的频繁重置定时器)。
三、架构与设计类反模式
3.1 过度封装自定义 Hooks
❌ 反模式:
为了“复用”,将只有两行代码的逻辑强行抽离成 useXXX,或者创建一个包含几十个状态的巨型 Hook。
- 过小:增加了文件跳转成本,无实际收益。
- 过大:违反了单一职责原则,导致组件只要用到其中一个状态就要重渲染。
✅ 建议:
遵循“三次法则”:同一段逻辑出现三次再考虑抽取。保持 Hooks 的粒度细小且专注,通过组合来构建复杂逻辑。
3.2 在 Provider 中直接传递字面量对象
❌ 反模式:
<MyContext.Provider value={{ user, logout }}>
{children}
</MyContext.Provider>
后果:每次父组件渲染,value 对象引用都会变化,导致所有消费该 Context 的子组件无条件重渲染。
✅ 修正:
const value = useMemo(() => ({ user, logout }), [user, logout]);
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
四、总结与自查清单
在 Code Review 或自我检查时,请问自己以下问题:
- Key:列表是否使用了稳定的唯一 ID?
- 引用:是否在 JSX 中直接创建了对象/函数?是否对 Context Value 使用了
useMemo? - 依赖:
useEffect和useCallback的依赖数组是否完整?是否存在闭包陷阱? - 计算:是否可以用直接计算替代
useEffect派生状态? - 规则:是否有条件调用 Hooks 的情况?
- 粒度:Custom Hooks 是否职责单一?Context 是否拆分得当?
避开这些反模式,不仅能提升应用性能,更能让代码逻辑清晰、易于维护。记住,最好的优化是写出符合 React 设计哲学的代码。