转换hooks的开发建议

2,924 阅读7分钟

同学,还不试一把React-hooks吗?

使用hooks开发有一阵时间了,几乎是从推出后就尝试使用,一路走来,一顿毒打,踩了不少坑,也逐渐体会到了hooks模式下,开发方式的变化,不得不说还是要吹一波的。

接触到的最开始就是MaterialUI了,当时hooks模式正式发布后,MaterialUI也推了一版,并且官方文档就已经使用到了hooks,当时以为hooks就是存个state,后来发现完全是另一种开发组件的思想。

本篇只是希望给同学们提供一些不一样的开发思路,更多是和大家讨论一下hooks模式,涉及到具体的api使用,及更深入可去React-Hooks学习。

Hooks模式介绍

2019年第一季度,React16.7正式发布,Hooks模式也可以用于生产环境中,

首先必须要明确的一点,hooks是在function组件中使用的,所以函数式组件的使用是直接调用的,比如

// React 16.13.0 ReactFiberHooks.new.js

// Component即jsx转换后传递的 Function组件的引用
let children = Component(props, secondArg);

但是基于ReactFiber架构,即有了为Function组件拓展的可能,在fiber上挂载一个hooks的链

个人认为,发布的hooks主要分为三大块方向

  • 存储数据型 (useState, useReducer ...)

主要负责将数据和'生命周期'连接起来,有了同class组件类似的this.state功能,能够满足函数式组件自己定义状态的能力。

  • 辅助记忆型 (useMemo, useCallback ...)

由于函数式组件每次都是重新执行一遍,所以自己存储的一些数据会丢失,需要重新来一次,比如使用到了一个计算较为昂贵的值,就可以使用该函数缓存起来。

  • 工具型 (useEffect, useRef, useContext ...)

使得函数式组件有了能够和React内部有了关联,比如能够对数据生命周期控制,或者能够获取到上下文Context,或是存储ref对象

Hooks解决了什么

hooks开发时有两点感觉还是很不错的,能和之前class组件有不同的开发方式

以数据为生命周期

之前使用class组件时,我们都是以整个组件的周期去统揽我们的各个数据,应该在这个组件什么生命周期去操作他,使用它,同样的,我们的核心是组件的生命周期,所有数据要应用到组件的生命周期,这就有一个问题,我们其实更加关心的是这个数据的生命周期。

比如我们展示一个好友列表,如果当前没数据,我们去从服务端拉起数据,同样的,如果这个数据发生了改变,需要更新好友列表时,我们需要从服务端拉取数据。那么实际上,我们就是在看这个好友列表,而且最终反应到界面,更不知道组件的生命周期状态发生了改变。

hooks模式下,则完全变成了我们对一个数据的生命周期的操作,不管当前这个组件是什么状态,只关心最终呈现到页面上,这也更符合我们的思考逻辑,同时也对开发者更加友好,减少bug的最好方式就是少写代码

以搜索框举例

我们需要通过一个搜索框中内容的改变做一些校验

  • Class组件中
this.state = {
  search: 'defaultValue',
  resultList: [],
}

// 为了放大问题,使用间接的方式修改resultlist
componentDidUpdate(prevState) {
  if (prevState.search !== this.state.search) {
    this.setState({
      resultList: fetchData(this.search)
    })
  }
}


// 组件挂载好,去修改默认搜索内容
componentDidMount(){
  this.setState({
    resultList: fetchData(this.search)
  })
}
  • Function组件中
// 设定search搜索框中的内容状态
const [search, setSearch] = React.useState('defaultValue')
// 当search内容发生改变时,更新结果列表
const [resultList, setResultList] = React.useState([])
React.useEffect(() => {
  setResultList(fetchResult(search))
}, [search])

逻辑更为集中

当时用了class组件时,大部分逻辑是处于分散在各个生命周期内的,因为我们使用的类,所以必须是一个个方法,如果又要配合生命周期使用,那么state+logic+lifecycle会分散出来,然而大部分组件内状态的管理都会使用到这三个方式,能将某一部分集中起来,代码量再多的情况下,也会更好的拆分出来。

hooks开发相关建议

hooks不止是存储了this.state/this.setState

无需刻意在function组件中模拟class的周期

如果真的需要class组件周期使用的,那么使用class组件是更好的方式。

大部分人会模拟生命周期,比较明显的就是didMount

React.useEffect(() => {
  // 模拟componentDidMount
}, [])

其实可以写很多个,内部使用依赖进行比较,一个空的依赖始终都是一样的,所以只会执行一次。

// name相关操作
const [name, setName] = React.useState('')
React.useEffect(() => {
  // 初始化name操作...
}, [])


// age相关操作
const [age, setAge] = React.useState(0)
React.useEffect(() => {
  // 初始化age操作...
}, [])

使用callback时的闭包

当使用callback,他会缓存当前的执行栈相关的信息,这里的缓存如果控制不好依赖,就容易造成很大的问题,同样的window自带的setTimeout,setInterval也会有相关的问题。

比如我们需要在组件挂载3s后打印当前的state状态

const [count, setCount] = React.useState(0)

React.useEffect(() => {
  const timer = setTimeout(() => {
    console.log(count)
  }, 3000)

  // 根据需要进行卸载 return () => { clearTimeout(timer) }
}, [])

return (
  <button onClick={() => { setCount(count + 1) }}>increment</button>
)

即使同样都是用到了count,多次点击按钮,发现3s钟后打印出的count还是0,因为创建时已经绑定了环境,及count已经为0了,(使用class组件并没体现出来,this.state.count,一串引用)

使用引用去解决引用,ref

const [count, setCount] = React.useState(0)

const callbackRef = React.useRef()
React.useEffect(() => {
  callbackRef.current = () => {
    console.log(count)
  }
}, [count])

React.useEffect(() => {
  setTimeout(() => {
    callbackRef.current()
  }, 3000)
}, [])

最终由于count的改变,修改了callbackRefcurrent,并且timeout绑定的是执行栈的callbackRef,然而他的ref已经被跟新了

不要欺骗你的hooks

hooks的依赖项决定了当前这个hooks是否在组件渲染时重新更新,即绑定了上下文环境中的变量,所以当他缓存起来时,就已经决定了内部的各种state值,可以说已经被替换为相应的数值,不再是一个变量了。

// 组件申明时
const [count, setCount] = React.useState(0)
React.useEffect(() => {
  // count ...
}, [])

// 后续组件被缓存,可以理解为
React.useEffect(() => {
  // 0 ...
}, [])

当使用各种callback去优化时,一定要注意使用的依赖是否正确

hooks放置在函数组件顶部

官方说明

hooks内部的实现是挂载于fiber内的一个链表,无论是useState,还是useEffect等,React无法用一个明显的key值去区分挂载于fiber上的具体哪一个节点对应哪一个hooks,使用他们的顺序index也就成了目前最好的选择

const [count, setCount] = React.useState()

React.useEffect(() => {
  // ....
}, [])

const price = React.useMemo(() => {
 // ....价格由一堆复杂折扣计算而来, f(discount)
}, [ discount ])

之后挂载与该Fiber上的hooks链表大致为


{ (count代表的useState) , next->(useEffect)}
{ (useEffect) , next->(useEffect)}
{ (count代表的useState) , next->(price代表的useMemo)}
{ (price代表的useMemo) , next->(null)}

正常情况React再次渲染时,根据出现的顺序,将Fiber上记忆的每一个hooks,依据顺序去赋值操作,正好也是对上的

如果有hooks前后出现顺序不一致,则会出现再次渲染时对不上,导致hooks调用错误

// 错误写法
if (!isLogin) {
  React.useEffect(() => {
    // .....
  }, [])
}

这里想表示,某一个effect逻辑只在登陆时做检查,但是由于当前这个hooks(useEffect)是被嵌套的,很可能出现该hooks在函数内的执行顺序不一致

  • isLogin == true

hooks对应情况

  const [count, setCount] ----> React.useState
    执行相关effect内部方法   ----> React.useEffect
  const price             ----> React.useMemo
  • isLogin == false

hooks对应情况

  const [count, setCount] ----> React.useState
  const price    		  ----> React.useEffect

这时出现了,对不上hooks存储的相关数据与其对应的使用发生了错误,即出现了问题

  • 正确的使用方式

将判断逻辑,嵌套内置与hooks内部

React.useEffect(() => {
  if (!isLogin) {
    // .....

  }
}, [])

无论程序执行状况是怎样的,最终都是稳定,正确的hooks调用关系

  const [count, setCount] ----> React.useState
    执行相关effect内部方法   ----> React.useEffect
  const price             ----> React.useMemo

需要立刻替换hooks吗

可以着手于新的组件使用hooks模式(如果喜欢这种开发方式),hooks模式与class模式是可以共存的,所以并不用着力去修改。