React 常见坑盘点
踩过这些坑,你的 React 才算入门
React 开发过程中有些坑,一旦踩过就印象深刻。今天来盘点和总结一下。
1. 闭包陷阱
这是 Hooks 最常见也最让人抓狂的问题。
现象
在 useEffect 或定时器回调里,读取到的 state 永远是旧值,怎么都更新不了。
原因
每次渲染都是独立的函数调用,闭包捕获的是那次渲染时的变量。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 永远是 0!
}, 1000);
return () => clearInterval(id);
}, []); // 空依赖,只跑一次
return <div>{count}</div>;
}
解决
必须严格填写依赖数组。ESLint 的 eslint-plugin-react-hooks 会警告你,千万别忽略它。
// 写法1:把 count 放进依赖
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(id);
}, [count]); // count 变了就重建定时器
// 写法2:函数式更新
setCount((c) => c + 1); // 不依赖外部变量
2. 用 index 作为 key
现象
列表渲染时偷懒,用数组下标作为 key:
items.map((item, index) => <Item key={index} {...item} />);
后果
当列表顺序变化(删除、移动)时,React 会错误地复用组件实例。
比如删掉第二项,结果第三项的内容跑到第一项去了,或者输入框里的值跑到别的行去了——因为 React 觉得"key 没变,这就是同一个组件"。
解决
用唯一 ID 作为 key:
items.map((item) => <Item key={item.id} {...item} />);
3. 直接修改 state
错误写法
// ❌ 错误!
state.count = 1;
后果
React 根本不知道数据变了,不会触发重新渲染。
正确写法
// ✅ 正确
setState({ count: 1 });
// 或者
setState((prev) => ({ count: prev.count + 1 }));
React 需要通过 setState 来知道你想要更新,它才能安排重新渲染。
4. 在渲染期间执行副作用
错误写法
function MyComponent() {
// ❌ 渲染期间直接发请求
fetch("/api/data").then((res) => setData(res));
// ❌ 渲染期间直接改 DOM
document.title = "Hello";
return <div>{data}</div>;
}
后果
Concurrent 模式下,React 可能会暂停/重启渲染,副作用可能执行多次,甚至阻塞渲染。
解决
把所有副作用都扔进 useEffect:
function MyComponent() {
useEffect(() => {
fetch("/api/data").then((res) => setData(res));
}, []);
useEffect(() => {
document.title = "Hello";
}, []);
return <div>{data}</div>;
}
5. 依赖对象导致无限循环
现象
依赖数组里放了个对象/数组,结果死循环了。
原因
每次渲染,函数组件里的对象都会重新创建(引用地址变了)。
// ❌ 会死循环
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// 每次渲染 options 都是新对象
someFunction(options);
}, [options]); // 依赖对象
// ...
}
渲染流程:
- 渲染 -> options 是新对象
- useEffect 发现依赖变了 -> 执行
- 执行过程中 setCount
- 触发重渲染
- options 又是新对象
- -> 回到步骤 1,死循环
解决
用 useMemo 缓存对象,或者只依赖对象里的具体原始值:
// 写法1:缓存对象
const options = useMemo(() => ({ name: "John" }), []);
// 写法2:只依赖原始值
useEffect(() => {
someFunction(options);
}, [options.name]); // 只依赖具体的值
总结
| 坑 | 后果 | 解法 |
|---|
| 闭包陷阱 | 读到旧值 | 严格填依赖数组 |
| 用 index 做 key | 列表错乱 | 用唯一 ID |
| 直接改 state | 不渲染 | 用 setState |
| 渲染期副作用 | 多次执行/阻塞 | 放 useEffect 里 |
| 依赖对象 | 死循环 | useMemo 或依赖原始值 |
这些坑踩过一个就记住了。祝你好运!