游乐游手机版
首页/AI热点日报/热点详情

AI生成React组件合并前必查的6个代码坏味道与示例对比

类型:热点整理2026-08-21
AI生成的React代码常见六个坏味道:渲染阶段可计算的逻辑不应放入useEffect;避免将props复制到useState导致不同步;useEffect中fetch需用AbortController处理竞态;列表key应使用稳定id而非index;memo组件传内联对象或函数会使其失效;勿滥用useMemo useCallback。合并前需人工审查这些细

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

AI 写的 React 组件,合并前必查的 6 个坏味道——每个都有代码对比

我自己每天都在用 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}
  • )}

); }

为什么坏

这里至少有三层问题:

  1. 会多触发一轮渲染。orders 一变化 → 组件先渲染一次(此时拿到的还是旧的 total/filtered)→ useEffect 执行 → setState → 再渲染一次。很多页面闪动、短暂空白,其实就来自这种写法。
  2. state 和 props 容易失去同步。哪天你在某个分支里只更新了 filtered,却忘了同步 total,页面数据立刻就可能出错。
  3. 依赖数组稍微写错就容易出 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:“这个位置永远代表这个元素。” 但实际业务里,位置变了,内容也可能已经变了。

常见后果包括:

  1. 如果列表项中有 ,用户正在输入时,上面插入一条数据,焦点可能立刻错位
  2. 如果列表项本身带内部 state,state 容易串位
  3. 动画、过渡效果和 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:

  1. 计算确实很重,比如大规模数据的 filter、sort、reduce
  2. 这个值或函数需要作为 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 到底写了什么。

来源:https://segmentfault.com/a/1190000048035422

相关热点

继续查看同栏目近期热点。

延伸阅读

补充最近整理过的热点入口。