阅读此文前假设你对 React 的新特性 Hooks 有一定的了解,并能进行最基本的使用。如果还不清楚 React Hooks 是什么,也没关系,建议读完本文后再去阅读下官方关于 React Hooks 的文档
今天,我们要来聊一聊 React Hooks 官方当前提供的 useXXX
到底够不够用,是否能满足我们日常的开发需求。
先说说最重要的 State
: 喂喂,useState 这么核心的 API,不就是专门用于处理 State 的吗?
是的,一点都没错。用过 useState
的同学应该都知道,原来的 this.setState
,现在可以用 useState
的返回值中的方法 setXXX
代替,如下所示:
const [ count, setCount ] = useState(0);
我们来简单对比一下 class
和 Hooks
写法的差别:
# get state
class: this.state.count
Hooks: count
# set state
class: this.setState({count: 1})
Hooks: setCount(1)
: 这个太简单了,了解过 React Hooks 的人都懂啊。所以你就是要给我讲这个?
当然,不是啦。我问你,有没有发现少了些什么呢?原来的 this.setState()
,是不是有第二个参数?这第二个参数是不是一个callback方法?这个方法是不是会在状态成功更新后调用呢?
: 你这么说好像是哦,但callback我没怎么用,你能说下使用的场景吗?
当然,信手拈来。
联动选择下拉框的场景,比如城市选择器。当选择了省份后,就需要去获取对应的城市列表,这个 moment 就会用到了。话不多说,先来看下 setState
的例子:
class App extends React.Component {
constructor() {
this.state = {
pId: 0
};
}
onProvinceChanged(val) {
this.setState({ pId: val }, () => {
fetchCities(this.state.pId);
});
}
...
}
想想如果没有callback,我们就比较难准确地在状态更新后进行一些其它的操作了
: 所以 React Hooks 对此束手无策?我不信,我不信,我不信。
当然,不是啦。可以用 useEffect
来实现这个需求。我们来看下使用 Hooks
方式如何实现上面的功能
function App() {
const [pId, setPId] = useState(0);
useEffect(
function() {
fetchCities(pId);
},
[pId]
);
function onProvinceChanged(val) {
setPId(val);
}
}
: 那就是能实现咯,所以你接下来到底想说什么呢?
你仔细看下上面的代码,是不是很像事件的模式,一个在监听事件(useEffect 在监听 pId 的变化,然后执行方法)一个在触发事件(setPId)。
事件模式可以起到解耦代码的作用,但同时意味着代码很松散,一方只负责触发事件,而不用去关心接下来会发生什么。但我们这里的需求很明确,我选择了省份,接下来肯定是要加载城市数据,这两步的逻辑是有关联的,有先后顺序的,当然希望挨得越紧越好,这样代码比较有条理性,阅读起来比较顺畅,容易理解。不信的话,你就认真对比下上面的两个例子仔细推敲一下。
反正我是觉得还是得用回 callback 的方式,功能实现了固然重要,但代码的可读性,可维护性也很重要。
: 但你不是说 useState 没提供 callback 吗?那现在能怎么做?
当然,官方目前确实没有提供,但我们可以通过自定义 Hooks 的方式来实现。接下来我们会使用到第三方开源库 nice-hooks 来满足我们的需求
把上面的例子用 nice-hooks 提供的方法重新写一遍,直接上代码,非常简单:
import { useStateCB } from 'nice-hooks';
function App() {
const [pId, setPId] = useStateCB(0);
function onProvinceChanged(val) {
setPId(val, newPID => {
fetchCities(newPID);
});
}
}
你看,callback 的回归,让 Hooks 在处理 state 方面至少保持与 this.setState
同等水平,不至于被吐槽落后了。
好了,Hooks 关于 state 的处理的介绍就告一段落了。
-------- ☕ 建议休息片刻,眺望远方 --------
接下来聊聊 Life Cycle 生命周期
我们都知道,每个组件都有它从生到死的生命周期。在 React 中,用的比较多的是:componentDidMount
,componentDidUpdate
,componentWillUnmount
。
: React Hooks 也应该有与之对应的 useXXX 方法吧?
我也觉得是,但现实是我们并没有发现官方有提供与之对应的 useXXX 方法。我知道你想说什么,放心,依然可以用官方当前提供的 API 来实现这些生命周期。接下来我们就一个一个来讲解。
- componentDidMount
该生命周期方法会在组件挂载到DOM树后执行,我们可以利用 useEffect
来达到这个目的,怎么弄呢?看下例子吧
useEffect(() => {
console.log('Do something, such as fetching data');
}, [])
传个空数组,代表依赖项不变,所以只会执行一次,所以在组件第一次渲染完毕后执行一次,就相当于 componentDidMount 了
- componentWillUnmout
在组件即将销毁时会执行该周期函数,那么对应的,我们依然可以利用 useEffect 来达到这个目的,看下例子:
useEffect(() => {
console.log('Do something, such as fetching data');
return function() {
console.log('Do something before destroyed')
}
}, [])
因为销毁动作在整个生命周期中也只会执行一次,所以我们可以在第一个例子的基础上增加一个返回函数,该函数会在组件销毁的时候执行
- componentDidUpdate
每当组件的 props,state 变化时,就会执行该周期函数,依然可以使用 useEffect 来达到该目的
useEffect(() => {
console.log('Do something when props / state changes')
})
这里并没有提供依赖值,所以在每次渲染后都会执行,类似于 componentDidUpdate。但这里有点小问题,就是在初始化时,这里也会执行,即这里会包括初始化,需要额外写些代码来支持,这里就不展开了。
我们还可以使用 useEffect
来达到 watch 某个 state 或 props 变化的目的。所以你会发现,生命周期被混在了一堆 useEffect
代码中,不是那么的直接了当。
虽然 useEffect
可以实现各种生命周期方法,但依然还是那个问题,代码的可读性和可维护性很重要。我们仍然可以用 nice-hooks ,使用它的 useLifeCycle
方法来让生命周期方法回归,用法非常简单,代码一目了然,就不啰嗦了,免得有凑字数嫌疑
useLifeCycle({
didMount() {
console.log('Do something, such as fetching data');
},
didUpdate() {
console.log('Do something when props / state changes')
},
willUnmount() {
console.log('Do something before destroyed')
}
});
另外,class
组件写法的生命周期方法有个小缺陷,就是当需要在注销时销毁初始化时声明的一些东西,如事件监听,如定时器,注册和销毁的逻辑被强行拆开,容易疏忽而导致Bug,所以 useLifeCycle
提供了一个 didMountAndWillUnmount
配置来写成对的逻辑,如下
useLifeCycle({
didMountAndUnmount: [
{
didMount() {
console.log('register foo event)
},
willUnmount() {
console.log('unregister foo event)
}
},
{
didMount() {
console.log('register bar event)
},
willUnmount() {
console.log('unregister bar event)
}
}
]
})
那么推荐的做法就是需要写成对逻辑的写在 didMountAndWillUnmount,而不需要作销毁处理的初始化操作就写在 didMount,willUnmount这里就写一些没有成对逻辑的销毁操作
-------- ☕ 建议休息片刻,听听音乐 --------
最后谈谈 Instance Variables 实例变量
在使用 Hooks 写组件时,因为现在是纯函数组件,所以没法像 class
一样声明实例变量,以下使用变量是会有问题的
function comp() {
let name = 'daniel';
}
: 看着挺正常的啊,函数内声明变量再正常不过的了。有啥问题呢?
你可能会在某个地方修改了 name 的值,并希望使用 name 变量时它的值是最后修改的值。可惜事与愿违,因为每次组件重新渲染时,渲染函数都会重新执行,那该变量就会被重新初始化。
: 好像是哦,现在就只剩下一个 render 函数,每次都会重新执行,那昨办呢?
我们可以利用 useRef
这个官方 hook,它的 current
属性会一直保持最后的值,如下:
function comp() {
const nameRef = useRef('daniel');
function someFn() {
// get
let name = nameRef.current;
// set
nameRef.current = 'sarah';
}
}
一旦我们修改了 current
属性的值,那么下次重新渲染时,该 current
值会保持最后修改的值,即达到了实例变量的效果。
: 按照上面的套路,你肯定会说代码可读性不太好之类的
哎呀,台词都被你抢了。但我仍然还是要说,虽然这样利用 useRef
是达到了目的,但代码看起来总是不那么友好。
这里依然建议使用 nice-hooks,它的 useInstanceVar
hook,用法与 useState 一样,差别就是 setXXX 的时候只是改变实例变量的值而已,并不会导致重新渲染,示例如下:
function comp() {
const [nameVar, setNameVar] = useInstanceVar('daniel');
function someFn() {
// get
nameVar;
// set
setNameVar('sarah');
}
}
建议在声明变量名时,使用 Var
作为后缀以区分开 state,如 [ xxxVar, setXXXVar ]
====== 华丽丽的结束分割线 ======
以上都是使用 nice-hooks 这个第三方开源库来使 React Hooks 更好用。
如果你有好的建议,请多给这个开源项目提提issues;
如果觉得对你有用,那还请给这个开源项目加个星呗。
感谢阅读!