另一个例子,假设业务逻辑中用 useState 创建了引用类型的 state 变量,将该 state 引入 useEffect 依赖项中。当我们更新 state 时,调用 setState 都会返回一个新的 state 对象(或数组),都会触发一次 useEffect 的重渲染。这就引出了第二个问题,假设我们只希望 useEffect 监听 state 对象 (数组) 中某一个属性,只有当该属性的值发生改变时,才去触发函数组件的重新渲染,这该怎么办?
use lodash isEqual method for deep comparision
We have just removed the dependency array in our effect and use the lodash isEqual method instead to make a deep comparison. Unfortunately, we run into a new issue because of the missing previousUser value. If we do the same thing with a class component in ComponentDidUpdate lifecycle, we can easily have the previous state value.
🔥 useRef comes to rescue
React.useEffect(() => {
if (!_.isEqual(previousUser, user) {
console.log('You need to do exercise!')
}
})
...
}
export default Profile
2. useRef for saving the previous state
To keep track of the previousUser value, we save it to the .current property of useRef hook because it can survive even when the component rerenders. To do that another effect will be used to update the previousUserRef.current value after every renders. Finally, we can extract the previousUser value from previousUserRef.current, then we deep compare the previous value with the new one to make sure our effect only run when those values are different
> 为了跟踪 previousUser 值,我们将它保存到 useRef 钩子的 .current 属性中,因为即使组件重新渲染它也能存活。 同时要保证每次渲染后都及时地更新 previousUserRef.current 值,因此引入了 useEffect。 最后,我们可以从 previousUserRef.current 中提取 previousUser 值,然后我们将先前的值与新的值进行深度比较,以确保我们的效果仅在这些值不同时才运行
```js
const Profile = () => {
const [user, setUser] = React.useState({name: 'Alex', weight: 20})
React.useEffect(() => {
const previousUser = previousUserRef.current
if (!_.isEqual(previousUser, user) {
console.log('You need to do exercise!')
}
})
const previousUserRef = React.useRef()
React.useEffect(() => {
previousUserRef.current = user
})
...
}
export default Profile
extract effects to the custom Hooks
If you want to reuse the code, we can make a new custom hook. I just extract the code above to a function called usePrevious
import React from 'react'
import ReactDOM from 'react-dom'
import useDeepCompareEffect from 'use-deep-compare-effect' // 引入 useDeepCompareEffect
function Query({query, variables}) {
// some code...
// 像 useEffect 一样使用它,区别在于 useEffect 对依赖项做的是浅比较,而 useDeepCompareEffect 是深度比较。它可以有效处理引用类型的依赖项
useDeepCompareEffect(
() => {
// make an HTTP request or whatever with the query and variables
// optionally return a cleanup function if necessary
},
// query is a string, but variables is an object. With the way Query is used
// in the example above, `variables` will be a new object every render.
// useDeepCompareEffect will do a deep comparison and your callback is only
// run when the variables object actually has changes.
[query, variables],
)
return <div>{/* awesome UI here */}</div>
}
React - useEffect 依赖项数组填坑
参考文章: 开发随笔: useRef获取之前状态以及useEffect的坑小记
前言
在使用 useEffect 的时候,我们通常会将变量添加到依赖项数组中,来手动控制 useEffect 的触发。useEffect 依赖项可以帮我们避免不必要的重复渲染,但是也会导致一些问题。目前遇到最多的大致有三类:
问题1⃣️:依赖项数组中包含引用类型
由于 useEffect 对依赖项进行的是浅比较
Object.is()
,且 react 变量遵循 immutable 不可变原则,所以当引用类型作为依赖项时,react 判断上次与本次的值,返回的始终是 false,因此会不断触发重渲染。 举例:解决方法:
举个例子感受一下解决方法3⃣️:
我们希望只有当属性
b
发生改变时,才触发 useEffect。 传统方法,若直接传入 state (引用类型),每次调用 setState,都会返回新的 state 对象,都会触发 useEffect。 我们取state.b
作为依赖项,此时 useEffect 监听的就是一个值类型,尽管每次 setState 都返回新的 state 对象,但 useEffect 每次比较的都是state.b
的值,而state.b
是值类型,所以可以被Object.is()
正确比较。若state.b
没有发生变化,则 react 会跳过这条 useEffect。usePrevious
除上述方法外,还有一种方法看上去更加符合 react 应有的做法。从语义上理解,我们希望 useEffect 能够监听一个引用值且仅监听该引用值的某些属性,那么依赖数组中必然存放的是该对象,而不是将它们一个个解构出来。因此,另一种解决思路是:将上一次的引用的属性值与本次引用的属性值进行比较,若发生改变则执行后续的业务逻辑。 听上去不错,但是,上一次的引用从哪里获取呢?我们知道函数式更新的 useState,它可以调用
prev => {...}
来获取历史值。但显然 useEffect 并没有这个功能。答案是: useEffect + useRef 关于 usePrevious,我找到了两处说明:下面一段代码就是 usePrevious 的具体实现
它实现了将参数 value 保存的功能。usePrevious 首次调用时,接受一个 value (被包裹的目标),将该 value 添加到 useRef 中保存。useRef 的特性决定了其保存的值会贯穿整个组件渲染周期且保持不变,在下一次组件重新创建时,它会返回一模一样的存储值。
有人可能会问:既然 useRef 可以永久保留值,为什么还要加一个 useEffect 呢? 答:useEffect 是确保保存的值始终是上一次记录的关键。组件每次重新渲染时都会执行其组件函数,显然 usePrevious 也会被执行,此时 value 会再一次的传入 usePrevious,由于传入的 value 为引用类型,所以 usePrevious 内部的 useEffect 会被触发,
ref.current
值自然也就会替换为 value 的值,这就保证了 usePrevious 始终会跟踪到这一变化,返回最新的保存值。请注意:usePrevious 始终返回的是上一轮的结果,这是因为 usePrevious 代码中, useRef 和 return 语句都是在 render 函数发生前同步执行的,而 useEffect 内的函数是在 render 函数结束,整个页面渲染完成后执行,因此,每次调用 usePrevious 实际返回的都是之前保存的值,本轮传入的值还未被更新。
关于 usePrevious 的使用: 这里就直接参考Introduction to useRef Hook中的例子:
use lodash isEqual method for deep comparision We have just removed the dependency array in our effect and use the lodash isEqual method instead to make a deep comparison. Unfortunately, we run into a new issue because of the missing previousUser value. If we do the same thing with a class component in ComponentDidUpdate lifecycle, we can easily have the previous state value. 🔥 useRef comes to rescue
React.useEffect(() => { if (!_.isEqual(previousUser, user) { console.log('You need to do exercise!') } })
... }
export default Profile
然后这样去使用它:
use-deep-compare-effect
另一种深层比较 effect 依赖项的方式,一个第三方库。请参阅 use-deep-compare-effect 通过
npm install --save use-deep-compare-effect
安装。 以如下方式引入并引用:问题2⃣️:依赖项数组中存在多个变量时,一个依赖改变,整个 useEffect 就会重新执行。
实际上,依赖项数组中,一个依赖改变就触发整个 useEffect 是正确且必要的,因为一个 useEffect 里的业务逻辑必然是紧密耦合的。如果有一天,你发现你需要使依赖项中某个依赖改变而不去触发当前 useEffect,很明显,你显然不需要将该变量放入依赖。或者说,你的 useEffect 逻辑并没有达到最佳,它可以拆分成更细节的业务逻辑,每个相关的依赖项管理相关的逻辑即可。
问题3⃣️:依赖项频繁变动,导致 useEffect 被频繁触发
请参照官方文档如果我的 effect 的依赖频繁变化,我该怎么办? 显然它给出了最佳解释。简单来说,最常用的就是两种方法: