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]); // 依赖对象

  // ...
}

渲染流程:

  1. 渲染 -> options 是新对象
  2. useEffect 发现依赖变了 -> 执行
  3. 执行过程中 setCount
  4. 触发重渲染
  5. options 又是新对象
  6. -> 回到步骤 1,死循环

解决

useMemo 缓存对象,或者只依赖对象里的具体原始值

// 写法1:缓存对象
const options = useMemo(() => ({ name: "John" }), []);

// 写法2:只依赖原始值
useEffect(() => {
  someFunction(options);
}, [options.name]); // 只依赖具体的值

总结

后果解法
闭包陷阱读到旧值严格填依赖数组
用 index 做 key列表错乱用唯一 ID
直接改 state不渲染用 setState
渲染期副作用多次执行/阻塞放 useEffect 里
依赖对象死循环useMemo 或依赖原始值

这些坑踩过一个就记住了。祝你好运!