likes
comments
collection

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

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

前言

不知道大家有没有这种感觉,自从hooks出来之后,react的门槛越来越高了。在以前那个时代,写好react只需要关注生命周期和setState就可以,其他的数据模型就交给redux或者mobx。对于一般人而言,代码写的虽然不会多好,但是总算是不会出什么叉子,也能用。但是自从hooks出来之后,不同人写出来的代码可真是千差万别,有些甚至可以用惨不忍睹来形容。更有甚者,一行代码直接导致了整个组件崩溃。我甚至有时候都想,为了代码的生命安全,大家要不还是回到class component的时代算了。

但是显然,我们都不可能回到class component的时代了。所以,我觉得很有必要给大家再讲讲我理解的hooks。

hooks简介

hooks是react在16.8版本引入的新功能。hooks赋予了react全新的意义,解决了react长久以来未解决的两大问题:异步和状态复用。react官方在提到引入hooks的动机的时候也提到Hook使你在无需修改组件结构的情况下复用状态逻辑。

但是仅凭这些只言片语其实你很难真正理解hooks的伟大。很多初识hooks的人都会认为这仅仅只是另外一种意义上的class component。并且在实际的项目中,很多人也都是这样用的:一个大的function,里面包含了一堆的useState和useEffect,以及一个return,这就导致了组件的逻辑和渲染全都耦合在一起,一个逻辑复杂的组件代码量甚至有上千行之多,严重的影响了组件的可维护性。如果你,或者你的团队遇到了这样的问题,或许你需要认真的阅读接下里的内容。

react内置的hooks

react内置的hooks主要有以下几个:

  • useState
  • useEffect
  • useContext
  • useMemo
  • useCallback
  • useReducer
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue

上面这些api在你翻阅任何有关于react的hooks相关的资料的时候都能查询到,但是在实际项目中,我依然遇到很多人误用或者滥用这些hooks的行为。所以接下来我会用比较直白的语句给大家稍微再介绍下这些hooks。

useState

useState应该是大家用的最多的一个hooks。当你需要在fc中改变组件状态的时候,你就需要用到useState。语法我就不再赘述了,但是请记住一点,只有当你真正需要在组件内部改变某个状态的值的时候,你才需要用到useState。 如果你只是想要组件中记住某个变量的值,或许你需要的是useRef或者useMemo,这两个会更安全。

useEffect

useEffect除了useState以外另外一个频繁使用的hooks,用来监听组件的状态改变,相当于原先class component组件中的componentDidUpdate和componentDidMount这两个生命周期。稍微有点不一样的是,useEffect你可以在依赖数组里面设置你想要响应哪些状态的更改,而不是每次状态改变的时候都会被响应。所以相比于componentDidUpdate或componentDidMount,useEffect更容易组织你的逻辑,你可以将多个不同的状态响应写成多个useEffect,而不是在一个里面。例如:

const View = () => {
  const [name, setName] = useState()
  const [age, setAge] = useState()
  
  useEffect(() => {
    //age发生改变
  }, [age])
  
  useEffect(() => {
    //name发生改变
  }, [name])
  
  return (
    <div>
      {name}
      {age}
    </div>
  )
}

在上面代码中,我们不需要自己保存前后两次state的值来判断是哪个状态发生了改变。这点是之前class component所没有的优势。

另外还有一点,如果你在useEffect中返回了一个方法,这个方法会在该effect下一次被触发前触发,这就意味着每次更新或者卸载前,都会触发一次。实际工作中,我见过有人以为这个方法只会在组件卸载的时候触发,因此引发了一些奇怪的bug。

useContext

在介绍useContext之前,需要你对context有一些基本的了解。在react里面,如果你需要跨层级传递状态,你就需要用到context。而useContext可以让你在fc中更方便的使用context,仅此而已。

useMemo

useMemo允许你在组件中记住某个状态。例如,你想要在组件创建的时候生成一个自增的id,并且保存住:

const myId = useMemo(()=>++ID,[])

我见过很多useMemo和useState用混的情况,例如上面的代码有人会写成:

const [myId] = useState(++ID)

也没有什么问题,除了ID渲染的时候都会被无效自增一次外。但是更严重的是下面的写法:

const [myId,setMyId] = useState()
useEffect(()=>{
    setMyId(++ID)
},[])

这就意味着你的组件会多一次无效的渲染。所以请记住我们在开始介绍useState的时候说的,只有你的状态改变必须触发组件更新的时候,你才需要useState。如果你的某个状态是依赖别的状态改变而改变,那么你应该使用useMemo,而不在useEffect中监听这个状态再setState。例如,请避免下面的写法:

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

应该改成:

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

useCallback

useCallback是useMemo的另外一种封装,相当于useMemo(()=>fn,deps)。

如果你的方法需要传递给其他组件作为属性使用,或者需要被作为依赖传入其他hooks中,那么你需要用useCallback封装一下。其他情况下使用,并不能给你带来性能的减少。

useReducer

useReducer是相对来讲比较复杂的一个hooks,需要你对reducer有一定的了解。如果你是redux的用户,理解起来应该问题不大。如果你没有用过redux,你可能需要先了解下useReducer和redux了。

看官方文档就行了,没啥太多讲解的必要。实际项目中,我比较少见到有同学使用,可能是本身对useReducer不熟悉,以及可能也不是完全需要一定要用useReducer解决的场景。

useRef

很多人会误以为useRef是createRef的另外一种形式(什么,你不知道createRef?那当我没说),实际上,他们几乎没有任何联系。要完全掌握useRef你需要把它和ref分离开来,先忘记ref。useRef在fc里面主要有两个作用:

  1. 每个useRef的值都是唯一的,能改变的只有它的current属性的值。
  2. 在每次渲染的时候,除非你手动修改useRef的current值都不会变,这意味着你可以用useRef来记住一些不会对组件渲染有任何影响的变量。
  3. 你可以用useRef来获得子组件的引用,以此来调用子组件中暴露出来的一些方法。

useRef最典型的用法我就不在这里赘述了,我们讲下另外一种比较典型的用法:

const isAb = useRef(Math.randon() > 0.5)
const abHandler = useCallback(() => {
  if (isAb.current) {
    alert('ab')
  } else {
    console.log('ab')
  }
  isAb.current = !isAb.current
}, [])

在上面这个例子中,我们就用了useRef来保存isAb的值,这样的话即使abHandler不需要重新创建也依然可以拿到isAb最新的值。

useImperativeHandle

useImperativeHandle比较简单,主要是让你可以在fc组件中暴露方法给父组件调用。你可以通过给组件传递useRef来获得useImperativeHandle暴露出来的方法。useImperativeHandle通常需要搭配forwardRef使用。如果你不走寻常路,不通过ref属性来传递,不用forwardRef也是ok的。

useLayoutEffect

useLayoutEffect和useEffect的调用方法一样,唯一的区别是,useLayoutEffect会保证在dom更新完成之后再被调用。如果你需要在effect中获取dom状态错误的话,你可以考虑使用useLayoutEffect。

尽可能的避免使用useLayoutEffect,因为它会阻塞dom渲染。

记住,你应该在仅当时尝试过useEffect失败之后再切换到useLayoutEffect。

useDebugValue

useDebugValue是用的比较少的一个hooks,可以让你在 React 开发者工具中显示自定义 hook 的标签。

我们来看一个官网的demo:

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  // ...

 // 在开发者工具中的这个 Hook 旁边显示标签  
 // e.g. "FriendStatus: Online"  useDebugValue(isOnline ? 'Online' : 'Offline');
  return isOnline;
}

比较简单,就不多说了。

上篇小结

本来打算一篇搞定的,但是发现篇幅有点长,今天我们就先讲到这吧。关于hooks的介绍是结合了官方以及个人在长期项目中的一些理解,如果觉得和你的理解有不一样的地方,欢迎评论区交流。但是我相信,对大部分人而言,这肯定给你在日常的工作中带来一些帮助。