同学,还不试一把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的改变,修改了callbackRef的current,并且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模式是可以共存的,所以并不用着力去修改。