likes
comments
collection
share

react指北:你真的会用hooks吗-中篇

作者站长头像
站长
· 阅读数 55

再言

上篇文章中,我们简单的讲了下关于hooks的一些基本理解,接下来我们看看实际中,误用或者滥用hooks的几种情况。

hooks的误用

hooks误用的情况,根据我日常工作中代码总结,主要分为下面几类

useState错误的当成useMemo使用

不知道是不是团队整体水平问题,日常中我感觉很少有人会在每个合适的场景都正确的使用useMemo。或许是因为useState太强大了(是的,几乎你用useState就可以完成所有业务的开发),我看到很多明明应该用useMemo就解决的场景用了useState。

错误一

const [isAb] = useState(Math.random()>0.5)

上面这个例子中,我们看到只有一个isAb,没有任何的setIsAb之类的声明和操作。实际上,这行代码的目的也仅仅是在组件初始化的时候生成一个isAb的值。但是这行代码写在这里就会让人感觉很奇怪,因为实际上:Math.random()>0.5这行代码是每次渲染都执行的,但是state的值并不会发生变化;其次,你也不需要在isAb更改的时候去重新触发组件渲染。 所以这种情况下,你应该使用语义更明确的useMemo。

错误二

const [year, setYear] = useState()
const [age, setAge] = useState()
useEffect(() => {
  setAge(new Date().getFullYear() - year)
}, [year])

这个demo我们在上面一篇文章中总结过,因为你的age依赖year更新的,不存在直接需要直接设置age的场景,所以这时候你应该用useMemo,而不是useState。 正解如下:

const [year, setYear] = useState()
const age = useMemo(() => {
  return new Date().getFullYear() - year
}, [year])

这样的话,不仅语义更清晰,且组件会少一次render,性能也更好。

请记住,一定是这个状态改变需要触发组件重新渲染且,你有需要直接设置这个状态的场景时,才用useState。其他情况下,请考虑用useMemo。 错误二中,age就是year的一个派生状态,所以,不需要用useState。

useMemo和useCallback傻傻分不清楚

上面我们讲该用useMemo的情况下用了useState,实际中,我还见过一些该用useMemo的用了useCallback。

看下面代码:

const getAge = useCallback(()=>{
    return new Date().getFullYear() - year 
},[])
return <div>{getAge()}</div>

实际上,这里getAge的目的只是为了收敛age的计算逻辑。正常情况下,这样写的确也没什么问题,但是关键是,明明我们用性能更好的useMemo(不需要每次都重复执行getAge),为什么要使用useCallback呢?

**如果你的代码执行过程只是为了收敛一些计算逻辑,请使用useMemo。而这也是useMemo存在的意义。 **

你应该在确实需要执行某些逻辑来进行一些操作(特定时机的dom操作、事件响应、状态更新等等)的时候才需要用到useCallback。并且实际中,如果你擅于利用纯函数,其实你很少场景下需要用到useCallback。

useEffect依赖错误

useEffect应该是hooks的一个异常重灾区。useEffect的误用大致分为两类:一类是deps里面的依赖错误,不加或者少加等;还有一类循环依赖,导致组件一直render最后崩溃。下面,我们先讲下useEffect依赖错误的问题。

看代码:

  const [name, setName] = useState('')
  const [age, setAge] = useState(0)

  useEffect(() => {
    console.log(`current name is ${name}, and age is ${age}`)
  }, [name]) 

在上面代码中,你会发现你的组件在age更新的时候并没有打印出上面那行内容,原因就是你少了age参数。问题不大,加上去就行了。

但是我见多有些人,他分不清楚自己应该加哪些依赖,所以就干脆什么都不加,例如:

  const [name, setName] = useState('')
  const [age, setAge] = useState(0)

  useEffect(() => {
    console.log(`current name is ${name}, and age is ${age}`)
  }) 

这样代码虽然work,但是性能似乎却不太行了。并且很容易就引发下面的问题。

在useEffect更新了依赖的state,导致hooks重复执行。

我们直接来看代码吧:

  const [id,setID] = useState(0)
  const [name, setName] = useState('')

  useEffect(() => {
    uploadUser({name,id})
    setName('')
    setID(++id)
  },[name,id]) 

上面是一段比较常见的代码,我们在用户名称更新的时候上传用户信息,之后把name清空,但是就上面这段简单的代码实际上却是有问题的。

我们看到useEffect依赖了id,意味着id每次更新的时候useEffect都被被执行。而当你调用setID去更新的时候,的值再次发生变化,这个副作用会继续被执行,这样的循环会被无限持续下去,最终导致持续崩溃

怎么解决这个问题呢?

你可以把id重useEffect的依赖中移除:

  const [name, setName] = useState('')

  useEffect(() => {
    setID((id)=>{
       uploadUser({name,id})
       return ++id
     })
  },[name]) 

useCallback依赖错误

useCallback的依赖错误也和useEffect的也差不多,应该是大部分的hooks都会有这个问题,其实也不需要多讲。但是还是想讲一下的呢,很多人在实际业务中发现useCallback的参数和state或者props的值不一致,明明我state或者props中某个状态已经更新了,但是useCallback里面的值却还是之前的,原因就是因为你没有把这个状态加到useCallback的依赖里面。

代码就不上了,建议你们可以装一个eslint插件来帮你们解决这个问题

组件内函数改变的时候没有通知调用者

实际业务中,我们有一些情况会把组件内部声明的函数传递给第三方调用者(例如:定时器、dom事件或者一些需要手动绑定的自定义事件,这些情况下,函数不是作为属性传递给另外一个组件调用,也不是在任何副作用里面执行,我将它成为第三方调用),这些情况下,如果函数改变应该通知第三方调用者重新调用,防止函数执行的时候没有调用到最新的变量。

看起来有些拗口,我们看代码:

const [current, setCurrent] = useState(0)
// 声明一个swiper
const swiper = new Swiper()
// 声明监听函数
const onChange = useCallback(
(index) => {
  console.log(`last page is ${current},current page is ${index}`)
  setCurrent(current)
},
[current],
)
//事件绑定
useEffect(() => {
  swiper.on('change', onChange)
}, [])

上面代码中,我们想要在每次swiper切换的时候打印出上一页和下一页,但是运行你会发现,last page每次打印出来的值都是0,而不是你预期的结果。原因就在在于,current改变的时候会重新生成一个新的onChange方法,但是,swiper绑定的却是组件初始化时候生成的那个,而那个时候的onChange方法上下文里面current的值就是0。

所以,你需要在每次onScroll方法改变的时候,重新绑定:

const [current, setCurrent] = useState(0)
// 声明一个swiper
const swiper = new Swiper()
// 声明监听函数
const onChange = useCallback(
(index) => {
  console.log(`last page is ${current},current page is ${index}`)
  setCurrent(current)
},
[current],
)
//事件绑定
useEffect(() => {
  swiper.on('change', onChange)
  return ()=>{
    swiper.off('change', onChange)
  }
}, [onScroll])

如果你不想每次都重新绑定事件,你也可以借助useRef,把代码改成下面的例子:

const [current, setCurrent] = useState(0)
// 声明一个swiper
const swiper = new Swiper()
// 声明监听函数
const changeHandlerRef = useRef(null)
changeHandlerRef.current = (index) => {
  console.log(`last page is ${current},current page is ${index}`)
  setCurrent(current)
},
const onChange = useCallback(
  (index) => {
    changeHandlerRef.current()
  },
  [],
) 
//事件绑定
useEffect(() => {
  swiper.on('change', onChange)
}, [])

可能是因为这种场景很常见,所以现在react也新加了一个hooks,useEvent来解决这个场景,而useEvent的核心,就是我们上面这段代码(我没有去看过useEvent的源码,但是我猜应该没错,如果有不对的,麻烦评论区指正一下)。有兴趣的可以自行去检索useEvent的用法。

hooks的滥用

hooks滥用的情况其实没有误用那么严重,但是如果当你打开编辑器,看到一个组件密密麻麻的写满了各种use,一个组件代码量有上千行之多,我觉得,可能hooks被滥用了。

当然这并不完全是hooks的问题。因为在class component的时代,大组件的问题也存在。只是hooks的出现,让很多人习惯性的在一个function里面去完成所有的业务,逻辑和视图混杂在一起,问题被放大了。在class component那个时代,render和方法是分开的,问题不是那么严峻,而在fc的时代,每次render,都要重新走一边这些所有的函数或者变量生成,首先性能会变差;其次,当逻辑和视图完全混杂在一起的时候,代码的维护成本就变得比class component的大组件维护成本更高了。

所以要如何优化或者改变这种情况呢?

首先,代码规范上应该严格把控大组件的出现,主要组件的代码量超过200或者300行,就需要引起警惕,是否要拆分成多个小组件来减轻这个组件的职责;其次,我们应该用更多的纯函数,把函数的声明挪到组件外侧,通过传参的方式来达到调用的目的,这样的话不仅组件组件清晰,且可复用性更强;最后,我们应该更多的利用hooks的可以复用能力,把一些相关的逻辑封装了一个独立的自定义hooks。

上面的道理,很简单,但是真正能做好的团队,据我了解应该不多,甚至可以说是稀少。

如果你或者你的团队在自定义hooks这件事情上做的不好,别慌。下面一篇,我们就重点再来讲讲hooks封装的问题。

码字不易,给个赞吧。

转载自:https://juejin.cn/post/7150929995785830413
评论
请登录