上周五 code review,我一口气打回了 6 个 PR。全是 AI 生成的 React 代码,看着能跑,但细看全是雷。
不是说 AI 写代码不行 ------ 说实话,这 3 个月我们团队的开发速度确实快了 3 倍。问题在于:AI 生成代码的速度是人类能审的 10 倍。当 review 跟不上产出,技术债就开始指数级堆积。
最近掘金热榜也在讨论这事 ------ "React 正在被 AI 投毒"。我不完全同意"投毒"这个说法,但如果你也在用 Cursor、Copilot 或者各种 AI Agent 写 React,下面这 5 个坑你大概率踩过。
先说结论
| 坑 | 典型症状 | 影响 | 自动化检测 |
|---|---|---|---|
| 无意义 re-render | 每次 props 都是新对象 | 页面卡顿 | ESLint 插件 |
| 状态管理混乱 | 啥都往 useState 里塞 | 维护噩梦 | CR 规范 |
| 密钥硬编码 | API Key 直接写字符串 | 安全事故 | git-secrets |
| useEffect 滥用 | 一个组件 5 个 effect | 竞态 bug | strict mode |
| 缺失错误边界 | 子组件一崩全崩 | 白屏 | ESLint 规则 |
有研究数据显示,AI 生成的代码引入 XSS 漏洞的概率是人写的 2.74 倍 ,硬编码密钥的概率是 2.1 倍。这不是危言耸听。
坑一:无意义 re-render
AI 特别喜欢在 JSX 里直接写内联对象和箭头函数。看着没毛病,但每次渲染都创建新引用,子组件全部重新渲染。
tsx
// ❌ AI 最爱写的代码
function UserList({ users }) {
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
style={{ marginBottom: 16, padding: '12px 20px' }}
onClick={() => handleClick(user.id)}
config={{ showAvatar: true, theme: 'light' }}
/>
))}
</div>
);
}
三个坑点:style 内联对象、onClick 箭头函数、config 对象 ------ 每次渲染都是新引用。如果 UserCard 用了 React.memo,完全白费。
tsx
// ✅ 修正版
const cardStyle = { marginBottom: 16, padding: '12px 20px' };
const cardConfig = { showAvatar: true, theme: 'light' };
function UserList({ users }) {
const handleCardClick = useCallback((userId: string) => {
handleClick(userId);
}, []);
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
style={cardStyle}
onClick={handleCardClick}
userId={user.id}
config={cardConfig}
/>
))}
</div>
);
}
实测数据:一个 500 条列表的页面,修完这一个问题,滚动帧率从 24fps 涨到 58fps。
坑二:状态管理灾难
让 AI 写一个表单页面,它会给你搞出七八个 useState:
tsx
// ❌ AI 的 useState 大法
function OrderForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [address, setAddress] = useState('');
const [city, setCity] = useState('');
const [zipCode, setZipCode] = useState('');
const [country, setCountry] = useState('CN');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
const [touched, setTouched] = useState({});
// ... 还有更多
const handleSubmit = async () => {
setIsSubmitting(true);
setError(null);
try {
await api.createOrder({ name, email, phone, address, city, zipCode, country });
} catch (e) {
setError(e.message);
} finally {
setIsSubmitting(false);
}
};
}
10 个独立的 state,改一个字段触发一次渲染,提交按钮的状态和表单数据混在一起。后面要加验证逻辑?地狱模式。
tsx
// ✅ useReducer + 关注点分离
interface OrderState {
data: OrderFormData;
status: 'idle' | 'submitting' | 'error';
error: string | null;
}
const initialState: OrderState = {
data: { name: '', email: '', phone: '', address: '', city: '', zipCode: '', country: 'CN' },
status: 'idle',
error: null,
};
function orderReducer(state: OrderState, action: OrderAction): OrderState {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, data: { ...state.data, [action.field]: action.value } };
case 'SUBMIT_START':
return { ...state, status: 'submitting', error: null };
case 'SUBMIT_SUCCESS':
return { ...state, status: 'idle' };
case 'SUBMIT_ERROR':
return { ...state, status: 'error', error: action.error };
default:
return state;
}
}
function OrderForm() {
const [state, dispatch] = useReducer(orderReducer, initialState);
const handleFieldChange = useCallback((field: string, value: string) => {
dispatch({ type: 'UPDATE_FIELD', field, value });
}, []);
// ...
}
一个 state 对象管所有字段,状态变更可追踪可调试。加验证?在 reducer 里统一处理。
坑三:密钥硬编码
这是最危险的。AI 写 API 调用示例时,经常直接把 key 写死:
tsx
// ❌ 你以为 AI 不会写这种?它会的
const openai = new OpenAI({
apiKey: 'sk-proj-abc123xyz...',
dangerouslyAllowBrowser: true // 双重作死
});
// 或者藏在配置对象里
const config = {
firebase: {
apiKey: "AIzaSyC1234567890abcdef",
authDomain: "myapp.firebaseapp.com",
}
};
团队里有个实习生直接把 Cursor 生成的代码 push 了。带着 OpenAI key。幸好我们有 git hook 拦住了。
防线:
bash
# 安装 git-secrets
brew install git-secrets
# 配置项目
cd your-project
git secrets --install
git secrets --register-aws
# 自定义规则:拦截常见 API key 模式
git secrets --add 'sk-[a-zA-Z0-9]{20,}'
git secrets --add 'AIzaSy[a-zA-Z0-9_-]{33}'
git secrets --add 'ghp_[a-zA-Z0-9]{36}'
# pre-commit 自动拦截
git secrets --scan
坑四:useEffect 地狱
AI 对 useEffect 有种执念。你让它写个数据加载组件,它能给你整出一坨嵌套的 effect:
tsx
// ❌ AI 的 useEffect 大乱炖
function Dashboard() {
const [user, setUser] = useState(null);
const [orders, setOrders] = useState([]);
const [stats, setStats] = useState(null);
useEffect(() => {
fetchUser().then(setUser);
}, []);
useEffect(() => {
if (user) {
fetchOrders(user.id).then(setOrders);
}
}, [user]);
useEffect(() => {
if (orders.length > 0) {
calculateStats(orders).then(setStats);
}
}, [orders]);
useEffect(() => {
if (stats) {
document.title = `Dashboard - ${stats.totalRevenue}`;
}
}, [stats]);
// 4 个 effect 链式依赖,竞态条件随时爆炸
}
四个 effect 形成隐式依赖链。用户快速切换页面?竞态。网络慢一点?数据错位。
tsx
// ✅ 用 React Query / SWR 替代手写 effect
function Dashboard() {
const { data: user } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
});
const { data: orders = [] } = useQuery({
queryKey: ['orders', user?.id],
queryFn: () => fetchOrders(user!.id),
enabled: !!user,
});
const stats = useMemo(
() => orders.length > 0 ? calculateStatsSync(orders) : null,
[orders]
);
useEffect(() => {
if (stats) document.title = `Dashboard - ${stats.totalRevenue}`;
}, [stats]);
}
数据获取交给专业库处理缓存、竞态、重试。同步计算用 useMemo。useEffect 只剩真正的副作用。
坑五:错误边界缺失
AI 写的组件几乎从来不加 Error Boundary。一个子组件崩了,整个页面白屏。
tsx
// ❌ 裸奔组件树
function App() {
return (
<Layout>
<Sidebar /> {/* 这里崩了 */}
<MainContent /> {/* 跟着白屏 */}
<NotificationBar /> {/* 也白屏 */}
</Layout>
);
}
tsx
// ✅ 关键区域加 Error Boundary
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<Layout>
<ErrorBoundary fallback={<SidebarFallback />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
<MainContent />
</ErrorBoundary>
<ErrorBoundary fallback={null}>
<NotificationBar />
</ErrorBoundary>
</Layout>
);
}
一个组件崩不影响其他的。通知栏崩了?静默降级。核心内容崩了?显示友好错误页。
自动化防线搭建
光靠 code review 是扛不住 AI 产出速度的。必须上自动化:
1. ESLint 规则(React 专项)
json
{
"rules": {
"react/jsx-no-constructed-context-values": "error",
"react/no-unstable-nested-components": "error",
"react/no-object-type-as-default-prop": "error",
"react-hooks/exhaustive-deps": "warn",
"no-secrets/no-secrets": "error"
}
}
2. Vercel React Best Practices
Vercel 开源了一套 40+ 条 React 性能优化规则(react-best-practices),可以直接喂给 AI Agent 当约束:
bash
# 克隆到项目根目录
git clone https://github.com/vercel/react-best-practices .react-rules
# 在 AI Agent 的 system prompt 或 .cursorrules 中引用
echo "Follow rules in .react-rules/" >> .cursorrules
这比事后 review 高效多了 ------ 直接让 AI 在生成时就遵守规则。
3. CI 流水线集成
yaml
# .github/workflows/ai-code-check.yml
name: AI Code Quality Gate
on: [pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx eslint src/ --max-warnings 0
- run: git secrets --scan
- run: npx tsc --noEmit
- run: npm test -- --coverage --watchAll=false
PR 过不了这些检查?不管是人写的还是 AI 写的,一律打回。
踩坑记录
写这篇文章过程中总结的几个教训:
- 不要让 AI 一次生成整个页面。越大的上下文,AI 越容易「自由发挥」。一个组件一个组件来,每个都 review。
- React Strict Mode 必开。它能帮你抓到 80% 的 useEffect 问题。开发环境双重渲染虽然烦,但能暴露很多隐藏 bug。
- AI 不会主动告诉你它不懂。它会自信地写出看似合理但逻辑有坑的代码。特别是涉及并发、竞态、状态同步这种,人类直觉很重要。
- 不要删 AI 的注释。AI 生成代码时经常带注释,这些注释虽然有时啰嗦,但在 review 时能帮你理解它的意图。等 review 完再清理。
小结
AI 写 React 代码不是洪水猛兽,但也绝不是「生成即可用」。
我们团队现在的做法是:AI 写初稿 → ESLint + git-secrets 自动拦截 → 人工 review 核心逻辑 → 合并。3 个月下来,这套流程跑得还算顺畅,AI 生成的代码采纳率稳定在 70% 左右。
最核心的一句话:把 AI 当初级工程师用,别当架构师用。它干活快,但需要你把关。
如果你也在团队里推 AI 编程,欢迎评论区聊聊你踩过的坑,看看大家的经历有没有重叠 😄