1. 起因
项目从 class component 迁移到 hooks 有一阵子了,useEffect 用得越多,越觉得它比生命周期函数难缠。最近一个列表页面出现了”接口被重复调用”的问题,排查完发现是依赖项数组没写对,顺手记一下几个容易犯的错。
2. 依赖项少写了,闭包拿到的是旧值
最常见的坑:eslint 的 exhaustive-deps 规则不开,或者开了但手动忽略掉,然后依赖项里少写了某个变量。
1 | function SearchList({ keyword }) { |
keyword 变化时组件会重新渲染,但 useEffect 因为依赖数组是空的,只在挂载时跑一次,fetchList 里闭包捕获的永远是第一次渲染时的 keyword。表现出来就是”搜索框输入了新关键字,但列表没更新”,一开始还以为是接口没触发,其实是根本没执行到里面那行代码。
结论就是老生常谈的一句话:把 eslint-plugin-react-hooks 的 exhaustive-deps 打开,别手动忽略。它报的警告十次有九次是对的,剩下一次可以用 useCallback/useRef 之类的方式正面解决,而不是直接注释掉规则。
3. 依赖项对象/数组每次渲染都是新引用,导致死循环或重复调用
反过来的坑是依赖项写多了,但依赖的是一个每次渲染都会重新创建的对象:
1 | function UserPanel({ userId }) { |
options 是字面量对象,每次渲染函数体重新执行都会创建一个新的引用,useEffect 用 Object.is 比较依赖项,引用不同就认为变化了,于是每次渲染都会重新触发。如果 effect 里又调用了 setUser 这类会触发重新渲染的操作,很容易演变成死循环,或者接口被高频重复调用。
修法也很直接,要么把 options 用 useMemo 包一层,要么拆开成基础类型依赖:
1 | useEffect(() => { |
4. 清理函数忘了写,异步请求race condition
组件卸载或者依赖项变化前,上一次 effect 里发出去的异步请求可能还没返回。如果没写清理函数,请求返回时组件可能已经不在了,或者 keyword 已经变了,但 setState 还是会执行:
1 | useEffect(() => { |
没有这个 cancelled 标志位的话,快速切换关键字搜索时,如果慢请求比快请求后返回,最终显示的列表会是旧关键字的结果——这种问题在网速不稳定的环境下特别容易复现,本地开发环境网速快反而不容易发现。
5. 小结
useEffect 依赖项数组本质上是”告诉 React 这个副作用依赖哪些值”,不是可以随便塞或者随便删的可选项。踩过这几次坑之后,现在写 useEffect 前都会先问自己三个问题:依赖数组是不是把用到的变量都列全了?依赖项里有没有每次渲染都变的引用类型?有没有可能出现竞态的异步操作?