React Hooks 闭包陷阱:状态“丢失“的经典坑

一、问题现象

在 React 函数组件中,事件回调函数里获取的状态值不是最新值,而是初始值或旧值。

ts 复制代码
const [count, setCount] = useState(0);

// 点击按钮时,期望打印最新值,实际却打印旧值
const handleClick = () => {
  console.log(count);  // 可能不是最新值!
};

二、根因分析

2.1 闭包的基本特性

JavaScript 闭包会捕获定义时的变量值:

ts 复制代码
function createClosure() {
  let value = 0;
  
  return {
    getValue: () => value,  // 捕获 value 的引用
    setValue: (v) => { value = v; }
  };
}

const closure = createClosure();
closure.setValue(10);
console.log(closure.getValue());  // 10 ✅ 正常工作

2.2 React 组件的特殊情况

React 函数组件每次渲染都会创建新的函数作用域:

ts 复制代码
// 第一次渲染
function MyComponent() {
  const [count, setCount] = useState(0);
  
  // 这次渲染时,count = 0
  const handleClick = () => {
    console.log(count);  // 捕获 count = 0
  };
  
  return <button onClick={handleClick}>Click</button>;
}

// 第二次渲染(setCount(1) 后)
function MyComponent() {
  const [count, setCount] = useState(1);
  
  // 这次渲染时,count = 1
  // 但是!如果 handleClick 没有重新创建
  // 它仍然是旧的,捕获着 count = 0
}

2.3 问题示意

三、典型场景

场景 1:useEffect 依赖数组缺失

ts 复制代码
// ❌ 错误示例
useEffect(() => {
  const timer = setInterval(() => {
    console.log(count);  // 永远是 0
  }, 1000);
  return () => clearInterval(timer);
}, []);  // 缺少 count 依赖

// ✅ 正确写法
useEffect(() => {
  const timer = setInterval(() => {
    console.log(count);
  }, 1000);
  return () => clearInterval(timer);
}, [count]);  // 添加 count 依赖

场景 2:事件回调未更新

ts 复制代码
// ❌ 错误示例
const handleClick = useRef(() => {
  console.log(data);  // 捕获旧的 data
}).current;

// ✅ 正确写法 1:使用 useCallback
const handleClick = useCallback(() => {
  console.log(data);
}, [data]);  // 依赖变化时重新创建

// ✅ 正确写法 2:使用 useRef 同步最新值
const dataRef = useRef(data);
useEffect(() => {
  dataRef.current = data;
}, [data]);

const handleClick = () => {
  console.log(dataRef.current);  // 始终是最新的
};

场景 3:异步操作中的状态

ts 复制代码
// ❌ 错误示例
const handleAsync = () => {
  fetchData().then(() => {
    console.log(items);  // 可能是旧值
  });
};

// ✅ 正确写法 1:使用 useRef
const itemsRef = useRef(items);
useEffect(() => {
  itemsRef.current = items;
}, [items]);

const handleAsync = () => {
  fetchData().then(() => {
    console.log(itemsRef.current);  // 始终是最新的
  });
};

// ✅ 正确写法 2:使用函数式更新
const handleAsync = () => {
  fetchData().then(() => {
    setItems(prev => {
      console.log(prev);  // 获取最新值
      return prev;
    });
  });
};

四、解决方案对比

方案 适用场景 优点 缺点
添加依赖 简单场景 直观简单 可能导致频繁重建
useCallback 事件处理 性能可控 需要维护依赖数组
useRef 同步 复杂状态 避免重建 需要额外同步逻辑
函数式更新 状态更新场景 无需额外依赖 仅适用于 setState

五、最佳实践

选择正确的策略

代码示例

ts 复制代码
// ✅ 推荐:综合使用多种策略
function useLatestValue<T>(value: T) {
  const ref = useRef(value);
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref;
}

// 使用示例
function MyComponent() {
  const [data, setData] = useState([]);
  const dataRef = useLatestValue(data);
  
  const handleEvent = useCallback(() => {
    // 使用 ref 获取最新值
    console.log(dataRef.current);
  }, []);  // 依赖数组可以为空
  
  return <div onClick={handleEvent}>Click</div>;
}

六、总结

  • 问题本质 闭包捕获的是定义时的值,不是调用时的值
  • 触发条件 回调函数未随状态更新而重新创建
  • 解决思路 让回调函数能获取到最新值
相关推荐
用户2986985301434 分钟前
前端 Excel 处理进阶:React 中合并与取消合并单元格的实现
前端·javascript·react.js
触底反弹40 分钟前
面试被问 RAG 只能说出六个字?这篇用 100 行 Node.js 代码帮你彻底搞懂
javascript·人工智能·面试
Highcharts42 分钟前
@highcharts/react完整技术指南|新版Highcharts React使用、迁移、SSR、TS 优化全解
前端·javascript
老王以为1 小时前
Fiber 节点 —— 一个数据结构如何承载整个 React 运行时
前端·react native·react.js
gis开发之家1 小时前
vue3组合式函数(Composables)的艺术——将业务逻辑从UI组件中连根拔起
前端·javascript·vue.js·前端框架·vue3·前端工程化·前端架构
mONESY1 小时前
ReAct 智能体循环与 JavaScript 异步并发原理深度解析
react.js
liuniansilence1 小时前
一次讲透 H5 唤起原生 App:Scheme 协议、平台限制与生产级封装
前端·javascript
用户938515635071 小时前
从“问答机器”到“智能体”:AI Agent 是如何自主工作的?
javascript·人工智能·全栈
weedsfly1 小时前
前端懒加载全家桶:图片、路由、组件的按需加载
前端·javascript·面试
Kel1 小时前
多 Tab 聊天不翻车:纯前端实现跨标签页消息同步的三层协同架构
前端·javascript·人工智能