问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

React Hooks闭包陷阱问题及解决方案

创作时间:
作者:
@小白创作中心

React Hooks闭包陷阱问题及解决方案

引用
1
来源
1.
http://www.ferecord.com/react-hooks-closure-traps-problem.html

React Hooks的闭包陷阱是一个常见的问题,尤其是在处理异步逻辑和状态更新时更容易出现。本文通过一个实际案例,详细介绍了闭包陷阱产生的原因,并提供了三种解决方案:使用useRef、函数式更新和useReducer。

问题描述

需求很简单:一个卡片上有多个下载按钮,点击后请求文件地址。大概实现如下:

export default function App() {
  const [loadingMap, setLoadingMap] = useState({});
  
  const handleClick = async (key) => {
    setLoadingMap({ ...loadingMap, [key]: true });
    await delay(3000);
    setLoadingMap({ ...loadingMap, [key]: false });
  };

  return (
    <div className="App">
      <button onClick={() => handleClick("a")}>
        {loadingMap.a ? "下载中" : "下载a"}
      </button>
      <button onClick={() => handleClick("b")}>
        {loadingMap.b ? "下载中" : "下载b"}
      </button>
    </div>
  );
}

测试时发现问题:如果连续点击 a 和 b 按钮,当 b 下载结束时,a 按钮会被重置为”下载中“。这种情况的产生是因为闭包问题,每次渲染都有独立的状态上下文,导致在事件处理器中获取到的是旧的状态值。

解决方案

关键点在于:在执行状态更新时,如何能获取到最新的状态,而不是本次渲染时的状态。

方案一:useRef

useRef 是 React 开发者的老朋友,是解决 Hooks 问题的万金油。它能存储一个指向不变的变量。

代码做如下更改:

const loadingMap = useRef({});
const forceUpdate = useForceUpdate();

const setLoading = (key, value) => {
  loadingMap.current[key] = value;
  forceUpdate();
};

loadingMap 存在 ref.current 里,就永远能找到它了,所有的渲染里,它的指向都是唯一的。唯一的问题是,修改 ref.current 是不会触发重新渲染的,因此需要使用 forceUpdate 强制刷新。

方案二:setState 的函数式更新

函数式更新好,获取状态没烦恼。setState 可以接收一个函数作为参数,即函数式更新,我们把这个函数称为更新器,这种更新方式不会直接触发重新渲染,而是会把更新器推到一个队列里,最后经过计算统一触发一次更新。更新器的参数是上一个计算后该状态的值,可以理解为最新的状态。

代码做如下更改:

const setMap = (key, value) => {
  setLoadingMap((v) => ({
    ...v,
    [key]: value,
  }));
};

const handleClick = async (key) => {
  setMap(key, true);
  await delay(3000);
  setMap(key, false);
};

函数式更新有如下优点:

  • 总是可以拿到最新的状态
  • 没有依赖,如果要在 useEffect 中更新状态,但又不想因状态更新触发 effect 时,用函数式更新很合适
  • 批量更新时只会触发一次重新渲染,这可以避免不必要的重新渲染

方案三:useReducer

useReducer 的情况跟 ref 类似,它也是将状态的状态管理转移了,得以避免 Hooks 的闭包陷阱。

代码修改如下:

const reducer = (state, action) => {
  return { ...state, ...action };
};

const [loadingMap, dispatch] = useReducer(reducer, {});

const handleClick = async (key) => {
  dispatch({ [key]: true });
  await delay(3000);
  dispatch({ [key]: false });
};

某种程度上,reducer 被称为 Hooks 的作弊模式,它可以解耦业务逻辑和更新方法的关系。和函数式更新一样,它也不需要依赖当前渲染上下文的状态,因此很适合在以下场景使用:需要在 useEffect 这类需要传入依赖的 Hooks 中修改状态,但又不想因依赖改变触发 effect 的场景。

不过使用 useReducer 往往意味着更多的代码逻辑(action、reducer、dispatch... )。

总结

Hooks 的闭包陷阱是非常常见的问题,当代码中有【异步逻辑+get/setState】的场景时,需要注意可能产生闭包陷阱。本文的三种方法除了解决闭包陷阱问题,在批量更新、依赖诚实、避免不必要的 effect 等场景下也都提供了解决方案。

最后,希望 useEvent 尽快到来。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号