问题场景
"我用 Cursor 生成了一段 React 组件,本地跑得好好的,上线就报错。查了半天,AI 把 useEffect 的依赖数组写错了,造成死循环。" "Copilot 帮我写了一个表格组件,看起来没问题,结果客户反馈表格不更新数据......"
如果你也在用 AI 辅助写前端代码,多少都遇到过类似的"AI 幻觉 bug"。本文整理了 5 个最常踩的坑,以及如何快速定位和修复它们。
坑 1:React useEffect 依赖数组遗漏
现象
AI 生成的代码:
tsx
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, []); // ⚠️ AI 漏掉了 userId
页面首次加载正常,切换用户后数据不变。React 严格模式开发下跑了两遍,上线后就暴露了。
原因
AI 经常把 [] 当成"只运行一次"的默认写法,忽略了外部依赖。特别是当提示词中只提了"获取用户数据",没有说"随 userId 变化重新请求"时。
修复
tsx
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]); // ✅ 补上 userId
进阶方案 :用 useData 或 react-query 替代原始 useEffect,AI 也很容易写出带缓存的请求方案,比手动管理依赖更安全。
坑 2:CSS 类名冲突(Tailwind + 自定义样式混用)
现象
AI 生成的组件:
tsx
<div className="flex items-center card-container">
Tailwind 的 flex items-center 正常工作,但 card-container 这个 CSS Module 的类名没有被自动注入了------因为 AI 把 CSS Modules 的 styles.cardContainer 写成了字符串。
原因
AI 在同一个组件中混用了全局 CSS 、Tailwind 、CSS Modules 三种方案,且没有按项目规范统一。它只是根据见过的各种代码推断出"这样写应该可以",实际上在 CSS Modules 项目里,className="card-container" 不会被转换成哈希类名。
修复
tsx
// ✅ 统一使用 CSS Modules
import styles from './UserProfile.module.css';
<div className={`${styles.cardContainer} flex items-center`}>
通用原则:
- 项目用 Tailwind → 告诉 AI "只用 Tailwind,不许写自定义类"
- 项目用 CSS Modules →
className={styles.xxx} - 项目用 CSS-in-JS → 明确指定 styled-components 或 Emotion
坑 3:TypeScript 类型断言滥用
现象
AI 遇到类型报错时,最喜欢用的一招就是 as any:
tsx
const data = response.data as any; // 🤦 城门失火,殃及池鱼
const items = data.items.map((item: any) => ({
...item,
label: item.name ?? '',
}));
整段代码变成了无类型保护 的状态,后续任何人在此基础上开发,IDE 提示全变成 any,等于 TypeScript 白用了。
原因
AI 优化的目标函数是"代码能跑",而不是"类型安全"。它检测到类型错误时,会优先选择修复成本最低的方案------as any 是最便宜的,但也是危害最大的。
修复
tsx
// ✅ 正确方案:定义接口 + 类型守卫
interface ApiResponse<T> {
code: number;
data: T;
}
interface Item {
id: string;
name: string;
}
function isItemArray(data: unknown): data is Item[] {
return Array.isArray(data) && data.every(
item => typeof (item as Item).id === 'string'
);
}
// 使用时
const result: ApiResponse<Item[]> = response.data;
if (!isItemArray(result.data)) throw new Error('数据格式异常');
提示词技巧:在 prompt 中加上 "严禁使用 as any,必须定义完整类型"。
坑 4:状态更新依赖旧值
现象
AI 写的一个计数累加组件:
tsx
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
for (let i = 0; i < 5; i++) {
setCount(count + i); // ⚠️ 每次都是 0 + i
}
};
点击后 count 变成了 4,而不是期望的 0+1+2+3+4 = 10。因为 React 的状态更新是异步批处理 的,循环中的每次 setCount 都基于同一个 count 值。
原因
AI 不理解 React 状态更新的闭包陷阱 和批处理机制 。它写出的是直觉上的"每次加 i",但实际执行的是"连续 5 次 setCount(0+i)"。
修复
tsx
// ✅ 使用函数式更新
const handleClick = () => {
for (let i = 0; i < 5; i++) {
setCount(prev => prev + i);
}
};
同理也适用于 useReducer 和 Vue 的 ref/reactive------AI 经常写出直接读取最新值而非通过函数式更新的代码。
坑 5:错误处理------"只要不报错就算成功"
现象
AI 生成的 API 调用代码:
tsx
async function loadData() {
try {
const res = await fetch('/api/data');
const json = await res.json();
setData(json);
} catch (e) {
console.error(e);
}
}
看起来没问题?坑在中间:res.json() 可以抛出 JSON 解析错误、网络错误可以被 catch,但是 HTTP 400/500 不会触发 catch,会被当作成功处理!
原因
AI 的"认知盲区":HTTP 错误码(4xx/5xx)不会让 fetch 抛出异常,只有网络断连才会。AI 训练数据中大量包含 fetch → then → catch 的简单写法,但很少强调 res.ok 检查。
修复
tsx
async function loadData() {
try {
const res = await fetch('/api/data');
// ✅ 必须手动检查 HTTP 状态
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const json = await res.json();
// ✅ 再检查业务状态码
if (json.code !== 0) {
throw new Error(json.message || '业务异常');
}
setData(json.data);
} catch (e) {
// ✅ 给用户友好的反馈,不是只打 log
toast.error(e instanceof Error ? e.message : '请求失败,请重试');
}
}
要点总结
| 坑位 | AI 的典型错误 | 修复方案 | 提示词可预防 |
|---|---|---|---|
| useEffect 依赖 | 漏掉依赖项或乱写 [] |
补全依赖 / 改用 SWR | ✅ "考虑所有响应式依赖" |
| CSS 冲突 | 混用多种样式方案 | 统一规范、用 CSS Modules | ✅ "严格遵循项目 CSS 规范" |
| 类型安全 | 滥用 as any |
定义接口 / 类型守卫 | ✅ "严禁使用 any" |
| 状态更新 | 依赖旧值、闭包陷阱 | 函数式更新 / useReducer | 需要人工审查 |
| 错误处理 | 只 catch 不检查状态码 | 检查 res.ok + 状态码 | ✅ "处理 4xx/5xx" |
核心建议:
- AI 写的代码 必须逐行 review,特别是状态管理和异步逻辑
- 写提示词时明确约束("不用 any"、"检查 HTTP 状态码")
- 配合 ESLint + TypeScript strict 模式 自动拦截低级错误
- AI 擅长写样板代码 ,但业务逻辑和边界情况请自己把关
AI 是增速器,不是代驾。越是关键时刻,越要自己握住方向盘 🚗