使用 useReducer 和 useCallback 解决 useEffect 依赖诚实与方法内置&外置问题
本文是在:https://juejin.im/post/5ceb36dd51882530be7b1585 的基础上进行的探究,非常建议阅读原文
一、useEffect 依赖诚实问题的粗暴解决及带来的问题
之前的一个例子,在 useEffect 中直接执行 setInterval 导致依赖欺骗带来的很多问题,详细的内容请移步至:
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
console.log('render useEffect')
const id = setInterval(() => {
setCount(prevCount => prevCount + step);
setStep(step => step + 1);
console.log(`[] count is ${count}, step is ${step}`);
}, 1000);
return () => clearInterval(id);
}, [step]);
上面代码中,虽然通过 setStaet(prevState => prevState + 1)
这样的方式取消对 count 的依赖,但是一旦代码里面同时依赖了两个 state,就无法通过这种方式解决。
上面的代码中,最终解决的方案其实是在 useEffect 中依赖了 step
,这已经是依赖诚实,但是造成的结果是显而易见的:每次 step 的变动都会导致重新实例化一次 setInterval 。
二、使用 useReducer 解决依赖诚实问题
我们最终的目的是 useEffect 本身的依赖只有 [ ]
,以为只有 [ ]
我们才能保证组件实例挂载的时候只会执行一次 setInterval
首先我们的依赖关系是发生在 useState
上的(具体的是 setCount),如果能够解决 setCount 中本身对 count 和 step 的依赖关系是最好的。
而 react 的文档中,明确提出了 useReducer
是 useState
的替代方案
文档原文:
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
从这个介绍上来看,使用 useReducer 在上面的场景中是比 useState 更合适的。
useReducer hook 是 react 的内置 hook,在声明上如下:
const [state, dispatch] = useReducer(reducer, initialArg, init);
useReducer 本身接受的参数有三个:
- 一个 reducer:
(state, action) => newStat
(reducer 与 redux 中的概念其实一样) - 初始化值
- init 是用来进行惰性初始化的:
init(initialArg)
如果使用 redux ,本身我们就不会直接操作 state,而是通过 dispatch 去触发某些规则,因此 useReducer 本身也会返回一个 dispatch
1、声明一个 reducer
下面的 reducer 比较简单,处理了一下 increment
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return {
...state,
count: state.count + state.step,
step: state.step + 1,
}
default:
return state;
}
}
2、使用 useReducer 声明 state 和 dispatch
const initialState = {
count: 0,
step: 1
}
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
3、使用 dispatch 进行 state 的一些变更
一开始提出的代码改成下面:
useEffect(() => {
console.log('render useEffect')
const id = setInterval(() => {
dispatch({ type: 'increment' });
console.log(`[] count is ${state.count}, step is ${state.step}`);
}, 1000);
return () => clearInterval(id);
}, []);
4、效果:
首先我们实现了实例一次 setInterval 定时器,但是却能够时刻处理 count 和 step 的变化
在一次渲染闭包内,能够每次访问到 state 的最新值
5、依赖真的都诚实了么?
现在对 count 和 step 的两个依赖都剥离出去了,我们认为目前 useEffect 的依赖都是诚实的,其实不然。
因为我们最终还是依赖了 dispatch
,不是只有 state 才叫依赖
但是我们都知道 dispatch 本身是不会变化的,因此我们认为对 uesEffect 来说,依赖都是诚实的
三、useCallback 解决 useEffect 内部函数的依赖诚实问题
1、非 useEffect 内部函数引起的依赖欺骗
上面代码中我们发现,如果 dispatch 内部也依赖了某些变量,这个时候很容易造成依赖的欺骗问题。
为了解决这个问题,我们可能都需将其他函数写在 useEffect 内部才能借助 eslint-plugin-react-hooks
这个插件检查通过
可以针对思考下面代码:
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
const setCountNew = () => {
setCount(count + step);
setStep(step + 1);
}
useEffect(() => {
const tm = setInterval(() => {
setCountNew();
console.log(count, step)
}, 1000);
return () => { clearInterval(tm); }
}, []);
上面的代码只是将 setCount 和 setStep 这样的方法移到了 useEffect 外面,目前在 useEffect 中我们从代码上看(忽略 console)是没有 state 的依赖的,看起来是没问题。
eslint 插件只会扫描出 setCountNew()
而上面的输出结果只会输出一次,即使我们有定时器。定时器是一致在执行的,但是页面是不会变化的,因为每次在 setCountNew
的时候,拿到的 count 和 step 都是第一次渲染闭包的值,也就是 0 和 1
2、useCallback 解决依赖欺骗问题
有些情况下我们不能将函数都写在 useEffect 内部,会造成无法管理,代码也会臃肿。
useCallback 本身会返回一个方法,同时 useCallback 接收两个参数:
- 参数1:匿名方法,里面执行相关的逻辑
- 参数2:数据依赖,本身 useCallback 需要监听相关的依赖项,这些依赖项可以在上面的方法中使用
文档的说明:
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新
上面方法的改造如下:
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
const setCountNew = useCallback(() => {
setCount(count + step);
setStep(step + 1);
}, [count, step]);
useEffect(() => {
console.log('render useEffect')
const tm = setInterval(() => {
setCountNew();
}, 1000);
return () => { clearInterval(tm); }
}, [setCountNew]);
上面的改动中,除了我们使用 useCallback 声明一个 setCountNew 的方法,并且在 useEffect 方法本身用之外,useEffect 还依赖了 setCountNew
。
这个表示说明,当 setCountNew 发生变化的时候(本身如果 state 发生了变化则返回的方法也会发生变化)
输出结果:
我们可以发现,输出结果中,每次都会重新执行 useEffect ,因为对于 useEffect 来说,useCallback 的 memorize 回调已经发生变化,基于此,我们可以放心的认为 useEffect 中依赖都是诚实的。