先把话说明白:这篇文章不是在劝你别用 AI 写 React 代码。

我自己每天都在用 Claude Code 和 Cursor,开发效率至少能翻一倍。但有句不太讨喜的话还是得提前说——AI 生成的 React 代码,如果不经过检查就直接合并,风险往往比你以为的更高。
这并不是说 AI 不会写 React 语法。单论语法,它很多时候甚至比人更熟。
真正的问题在于,React 的“最佳实践”本质上是一系列强依赖上下文的判断:什么时候该用 useEffect,什么时候根本不该用;state 该放在哪一层;列表 key 应该怎么选;组件到底要不要包一层 memo。这些判断成立的前提,是这段代码所在的完整组件树和真实业务场景,而 AI 大多数时候只能看到你贴进对话框里的那一小段代码。
也正因为如此,下面这 6 个坏味道,几乎每天都会出现在 AI 生成的 React PR 里。我都配了实际的代码对比,合并前快速过一遍,基本就能发现问题。
1. 把渲染阶段能直接计算的数据塞进 useEffect
这是 AI 生成 React 组件时最常见的坏味道,几乎没有之一。
AI 常见写法
function OrderList({ orders }: { orders: Order[] }) {
const [total, setTotal] = useState(0);
const [filtered, setFiltered] = useState([]);
useEffect(() => {
const filteredOrders = orders.filter(o => o.status === 'paid');
setFiltered(filteredOrders);
setTotal(filteredOrders.reduce((sum, o) => sum + o.amount, 0));
}, [orders]);
return (
合计: {total}
{filtered.map(o => - {o.name}
)}
);
}
为什么坏
这里至少有三层问题:
- 会多触发一轮渲染。orders 一变化 → 组件先渲染一次(此时拿到的还是旧的 total/filtered)→ useEffect 执行 → setState → 再渲染一次。很多页面闪动、短暂空白,其实就来自这种写法。
- state 和 props 容易失去同步。哪天你在某个分支里只更新了 filtered,却忘了同步 total,页面数据立刻就可能出错。
- 依赖数组稍微写错就容易出 bug 甚至死循环。AI 写这种 useEffect 时,经常会漏依赖或多写依赖。
正确写法
这类派生数据,应该直接在渲染阶段计算,不要额外存 state,也不需要 useEffect:
function OrderList({ orders }: { orders: Order[] }) {
const filtered = orders.filter(o => o.status === 'paid');
const total = filtered.reduce((sum, o) => sum + o.amount, 0);
return (
合计: {total}
{filtered.map(o => - {o.name}
)}
);
}
只有在 filter 或 reduce 的计算量真的很大时(比如上万条数据加复杂逻辑),再考虑使用 useMemo。但不要一上来就过度优化。
判断标准
当你看到 AI 生成的 useEffect,先问自己一句:“这段逻辑,能不能直接写在 return 上面?” 大多数情况下,答案都是可以。
2. 把 props 直接复制到 useState 里
AI 常见写法
function UserForm({ user }: { user: User }) {
const [name, setName] = useState(user.name);
const [email, setEmail] = useState(user.email);
return (
<>
setName(e.target.value)} />
setEmail(e.target.value)} />
>
);
}
乍一看很合理:表单需要编辑,那当然就得有本地 state。
为什么坏
useState(user.name) 里的 user.name,只会在组件第一次挂载时读取一次。后面父组件即使传入了新的 user,name 也不会自动更新。
这个问题很隐蔽——本地开发时你切换用户也许能发现,但很多时候 AI 写完后,你只是跑一遍单用户流程,PR 就这么过了。
等上线以后,用户一切换,表单里还显示上一个人的信息,问题就直接暴露出来了。
正确写法
先明确表单到底是受控组件还是非受控组件。通常有两种做法:
A. 完全受控,state 放在父组件
function UserForm({ user, onChange }: {
user: User;
onChange: (patch: Partial) => void
}) {
return (
<>
onChange({ name: e.target.value })}
/>
onChange({ email: e.target.value })}
/>
>
);
}
B. 需要内部 state,但通过 key 强制重新挂载
当 user.id 变化时,React 会卸载旧组件并重新创建新组件,本地 state 也会重新使用新的初始值。
判断标准
看到 useState(props.xxx),十有八九都要警惕。通常要么把 state 提升到父组件,要么通过 key 做明确隔离。
3. 在 useEffect 里 fetch,却不处理竞态问题
AI 常见写法
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser);
}, [userId]);
return user ? {user.name}
: ;
}
这段代码的问题,你能一眼看出来吗?
为什么坏
当用户快速点击不同用户,比如 A → B → C 时,请求会依次发出,但返回顺序并不一定一致。
假设 A 的网络慢,C 已经先返回并展示了 C 的信息,结果 A 的响应稍后才到——页面就会被旧数据覆盖,突然又跳回 A。
这不是理论上的边角问题,而是前端在列表页、详情页里非常常见的真实坑。AI 通常不会主动帮你处理。
正确写法
可以使用 AbortController,或者使用 stale flag 方案:
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/users/${userId}`, { signal: ctrl.signal })
.then(r => r.json())
.then(setUser)
.catch(err => {
if (err.name !== 'AbortError') throw err;
});
return () => ctrl.abort();
}, [userId]);
return user ? {user.name}
: ;
}
如果想更省心,直接使用成熟的数据请求库也更稳妥,比如 SWR、React Query、TanStack Query,它们通常已经帮你处理了竞态和缓存问题。
判断标准
只要 useEffect 里出现异步请求,就应该顺手检查 cleanup。没有 cleanup,基本就可以视为一个明显的坏味道。
4. 列表渲染时用 index 当 key
AI 常见写法
{items.map((item, i) => (
))}
AI 写出这一行的时候,通常连停顿都不会有。
为什么坏
当列表发生排序变化、中间插入或删除元素时,React 会依赖 key 来匹配前后两次渲染的节点。你如果用 index 作为 key,等于是在告诉 React:“这个位置永远代表这个元素。” 但实际业务里,位置变了,内容也可能已经变了。
常见后果包括:
- 如果列表项中有
,用户正在输入时,上面插入一条数据,焦点可能立刻错位 - 如果列表项本身带内部 state,state 容易串位
- 动画、过渡效果和 transition 往往会出现异常
正确写法
应该使用数据本身稳定且唯一的 id:
{items.map(item => (
))}
如果后端数据确实没有 id,前端也可以在拿到数据时用 crypto.randomUUID() 或自增计数补一个,但前提是同一条数据必须始终对应同一个 id,绝不能每次渲染时重新生成。
判断标准
只要看到 key={index}、key={i}、key={idx},就应该立刻提高警惕。除非这个列表永远不会重排、不会增删、不会持有内部 state,否则都不安全。
5. 内联对象 / 内联函数 + memo 组件,导致 memo 失效
AI 常见写法
const Row = React.memo(({ style, onClick, data }: Props) => {
return {data.name}
;
});
function Table({ rows }: { rows: Row[] }) {
return rows.map(row => (
console.log(row.id)}
data={row}
/>
));
}
看起来像是做了性能优化:子组件用了 memo,好像就能减少重渲染。
为什么坏
style={{ padding: 8 }} 和 onClick={() => ...} 这类写法,每次渲染都会生成一个新的引用。
而 React.memo 默认使用 Object.is 做浅比较,只要新旧引用不相等,memo 就会失效。结果就是:父组件每次渲染,所有 Row 依然会重新渲染。
表面上像是在做 React 性能优化,实际上没有优化效果,反而额外增加了浅比较成本。
正确写法
静态对象可以提到组件外部:
const rowStyle = { padding: 8, borderBottom: '1px solid #eee' };
function Table({ rows }: { rows: Row[] }) {
const handleClick = useCallback((id: string) => {
console.log(id);
}, []);
return rows.map(row => (
));
}
const Row = React.memo(({ style, onClick, rowId, data }: Props) => {
return onClick(rowId)}>{data.name}
;
});
这里要注意:让 Row 内部再生成 () => onClick(rowId),是为了保证传给 Row 的 onClick 本身保持稳定引用。
判断标准
只要看到 React.memo,就顺手检查一下调用处传入的 props。如果传的是 {...}、[...]、() => ... 这种内联值,那么这个 memo 很可能等于白包。
6. 不加判断地滥用 useMemo / useCallback
这一条和上一条正好相反——AI 也很喜欢把 memo 相关 Hook 用得到处都是。
AI 常见写法
function Greeting({ name }: { name: string }) {
const greeting = useMemo(() => `Hello, ${name}!`, [name]);
const handleClick = useCallback(() => alert(greeting), [greeting]);
return ;
}
短短三行逻辑,硬是塞了两个 Hook。
为什么坏
useMemo 和 useCallback 本身也是有运行成本的:
- 每次渲染都要做依赖数组的浅比较
- React 内部要额外维护记忆槽
- 代码可读性明显下降,后续维护也更费劲
像字符串拼接这种操作,直接计算通常比 useMemo 更简单也更高效。而这里 button 的 onClick 也没有传给 memo 组件,useCallback 基本没有实际意义。
很多滥用 memo 的 React 代码,最终不仅没有更快,反而会更慢,因为该做的工作没减少,还额外多了一层比较和维护成本。
正确写法
function Greeting({ name }: { name: string }) {
const greeting = `Hello, ${name}!`;
return ;
}
通常只有下面两种情况,才值得考虑加 memo:
- 计算确实很重,比如大规模数据的 filter、sort、reduce
- 这个值或函数需要作为 prop 传给 memo 组件,或者会作为 useEffect / useMemo 的依赖项
除此之外,大多数场景里——不加反而是更正确的选择。
判断标准
看到 useMemo 或 useCallback 时,先反问一句:“如果把它删掉,哪里会多一次计算或多一次重渲染?” 如果答不上来,那通常就可以删。
最后:一份合并前 60 秒自查 checklist
如果你没时间看完整篇,把下面这段直接贴进你的 Code Review 模板:
□ useEffect 里的逻辑,能不能挪到渲染阶段?
□ useState(props.xxx) 出现了吗?出现就是错。
□ useEffect 里的 fetch,有没有 AbortController 或 cleanup?
□ 列表 key 是不是稳定 id?不是就改。
□ React.memo 组件的 props,有没有内联对象/函数/数组?
□ useMemo/useCallback,如果删掉会怎样?说不上来就删。
这 6 个问题,合并前快速扫一遍,1 分钟就够。
一句话总结
AI 生成的 React 代码,通常语法没问题,但 React 开发从来不只是语法正确。
它更考验“什么时候该做事、什么时候不该做事”的判断力。AI 看不到你的完整组件树,也看不到真实用户怎么交互,更看不到线上性能瓶颈和业务上下文——这些关键判断,现阶段仍然需要开发者自己负责。
用 AI 写 React 代码一点都不丢人,真正危险的是合并前从不检查 AI 到底写了什么。
