陷阱说明
众所周知, React的useEffect中必须要将状态放入依赖数组中才能在useEffect中拿到更新后的状态。
useEffect(() => {
console.log(count)
},[count])
那如果这个useEffect是用来绑定事件或者启动定时器的,而且这个事件或者定时器的回调函数需要用到最新的状态,这样做会导致事件的重复绑定或者定时器的重复启动,显然是有问题的。
useEffect(() => {
const interval = setInterval(() => {
console.log(count)
},1000)
return () => clearInterval(interval)
},[count])
// 每一次count更新都会再重新开启一个定时器
解决方案
这种情况的解决方案是回调函数ref化
- 将需要获取最新状态值的那部分代码抽取出来
- 定义一个ref存储这部分代码
- 每次更新时同时更新ref存储的代码
- 在回调函数中执行这个ref存储的代码
const callbackFn = useCallback(() => {
console.log(count);
setCount(pre => pre + 1)
}, [count]);
const callbackFnRef = useRef(callbackFn);
callbackFnRef.current = callbackFn;
useEffect(() => {
const interval = setInterval(
() => callbackFnRef.current()
, 1000)
return () => clearInterval(interval);
}, [])
原因是ref不参与react的状态系统,所以我们可以通过手动更新ref,从而避免useEffect依赖状态,解决这些问题。
封装hooks
在ahooks和usehooks库中已经把这类问题的解决方案封装成了hooks,理解了思路也是蛮简单的。
function useInterval(fn: Function, delay?: number | null) {
const callbackFn = useRef(fn);
useLayoutEffect(() => {
callbackFn.current = fn;
});
useEffect(() => {
const timer = setInterval(
() => callbackFn.current(),
delay || 0);
return () => clearInterval(timer);
}, []);
}