那啥,新年快乐
React在年底送给了各位React开发者一个年终大礼包,那就是React Hook
。如果还不了解的小伙伴,推荐大家看一下React Hook文档以及Dan的这篇Making Sense of React Hooks
这篇文章,不是教大家怎么用,因为是什么,怎么用,React文档都说的明明白白,如果你现在还没养成看文档的习惯,那么我建议你现在就去看一下。
新的思想
开头我其实是想讲讲useHook
给我带来的改变。
TL;DR 以前开发,是以行为作为划分依据来划分逻辑,代码之间过分耦合;现在,是以功能为维度去编写开发,更加各司其职,更易于测试。
这边以一个简单的列表页为例,说说我以前的写法和现在的写法。
假设我们要开发一个中台系统的商品列表页,那么它涉及几个模块
- 搜索模块
主要对商品的名称,金额之类的进行查询; - tabs标签页,这个标签页主要是将商品从流程开始到结束划分成几种状态,例如,从商品被买家下订单,到付款,到发货,到确认收货,这直接的各个流程状态,作为一个个标签页;
- 第三个模块也就是这个商品列表,包含一个表格,用来展示商品,以及一个分页器,也就是表格的页码之类。
组件就不谈了,直接说逻辑处理吧。这里总共有三个交互,搜索的时候点击查询按钮交互,切换tab按钮的交互以及表格分页器的交互。
以前的方法
以前我的开发逻辑,其实就是很单纯的,根据直觉的去开发。搜索我就包含两块,第一将搜索数据保存在state
里,紧接着执行数据请求;切换tab也是第一步将tab状态保存在state
里,紧接着执行数据请求;执行分页呢,就改变分页状态,紧接着执行数据请求。
伪代码如下,非常的直观:
// 查询按钮点击
function onSearchBtnClick(values) {
this.setState({values},this.query);
}
// tab标签页切换
function onTabChange(tabStatus){
this.setState({tabStatus}, this.query);
}
// 表格分页切换
function onPageChange(pageNo,pageSize){
this.setState({pageNo,pageSize}, this.query);
}
async function query(){
const payload = {
...this.state.values,
status: this.state.tabStatus,
pageNo: this.state.pageNo,
pageSize: this.state.pageSize
};
const data = await fetchList(payload);
this.setState({data});
}
// 通常刚进页面就要初始化数据
componentDidMount() {
// 其他操作...
this.query();
}
PS:setState第二个参数是个callback,该callback会在新的state真正被赋值时执行。
ok,到这边一直是我们常规的开发思路,看上去也没有任何问题。
现在我们做一个大胆的假设,来给上面这个页面脱掉一层衣服。 现在假设我们这个页面是一个没有数据请求的页面,就是单纯的点击搜索按钮就是将搜索表单状态保存,点击分页就是保存到分页状态,只是将所有的组件作为纯展示性的受控组件,代码会是什么样的:
// tab标签页切换
function onTabChange(tabStatus){
this.setState({tabStatus});
}
// 表格分页切换
function onPageChange(pageNo,pageSize){
this.setState({pageNo,pageSize});
}
// 组件
<SearchForm onClick={onSearchBtnClick} />
<Tab
status={this.statetabStatus}
onChange={onTabChange}
/>
<Pagination
onChange={onPageChange}
pageSize={this.state.pageSize}
pageNo={this.state.pageNo}
/>
PS:由于查询按钮并不包含任何交互,因此这边直接忽视
现在看一看,这是不是每个组件的交互逻辑最纯粹的样子,不包含任何的副作用。
ok,现在我提出新的需求了,要在页面刚加载的时候加载数据。
componentDidMount(){
query()
}
代码很简单,接下来我提出了新的需求,在用户点击查询按钮,切换分页,切换tab的时候进行数据请求,你该怎么做?
如果根据我们最初的逻辑,你是不是继续吭哧吭哧的在每个handle事件下面去增加query函数?那如果我继续加需求,在切换分页的时候动态改变document.title
呢?
为什么要将side effect
和正常的交互逻辑去柔和在一起呢。点击页面交互组件,将操作反馈响应到页面是handle的工作,把它们硬是根据用户的行为去柔和在一起,并不符合单一职责原则,并且也很不利于测试。除非这个按钮本身没有任何功能,专门用来加载数据,那说明逻辑很简单,简单的逻辑并不需要考虑这么多问题。
在这个例子中,我们应该将数据请求这一逻辑,和页面交互逻辑单独拆分出来思考。此时我们要想的是,有哪些情况会触发数据请求。注意我用的是触发,而不是执行。
很显然,当tab状态改变,分页改变,以及点击查询按钮的时候,就是触发数据请求的时候。
我举个生活中的例子,来区分根据行为划分东西以及根据功能划分东西的区别。
我们人都要吃饭,吃饭这一行为涉及到哪些东西,烧饭,烧饭要什么,要厨具;我们的菜要装吧,所以要餐具;吃完我要洗餐具,就要洗洁精抹布这些东西;吃完饭我可能有点口渴要喝口水,那就要杯子。这里如果根据行为划分,就会将厨具餐具洗洁精抹布和杯子这些东西全都放在一起;那根据功能划分呢?烧饭就是厨具,所有厨具放在一起;碟子碗筷这些作为餐具也放在一起,等等。前者是混乱的,而后者是高内聚低耦合的。假设我今天想烧饭但是我不想洗碗,我想让我妈洗,我只要去把厨具拿出来用就好了,而不是从一堆锅碗瓢盆里去找到底哪个才是我需要的。
同样,我洗碗这个逻辑,只要有脏的碗我可以洗;并且在后期我还可以将洗碗进行更深一步的抽象,抽象成洗,我不仅可以洗碗,我还可以洗衣服。
因此如果根据触发情况来写,最后我们的数据请求代码会变成这样:
componentDidMount() {
query()
}
componentDidUpdate(_, prevState) {
if(prevState.pageNo !== this.state.pageNo ||
prevState.pageSize !== this.state.pageSize ||
preState.tabStatus !== this.state.tabStatus ||
prevState.values !== this.state.values){
query()
}
}
大家也看到了,一个页面不只只有一个副作用,因此并不是每次的状态改变都需要触发query,所以我们需要很啰嗦的去判断状态是否改变。并且,同样一个数据查询,我们写在了两个函数里,并且这两个函数的逻辑,随着页面越来越复杂,也会越来越臃肿,于是React Hooks
应运而生
新的方法
useEffect(()=>{
query()
},[pageNo, pageSize, tabStatus, values])
上面就讲query这个逻辑从两个硬生生的生命周期钩子里脱离开,成为一个独立的逻辑。如果整个代码逻辑完全使用React Hooks
,会是下面这样:
function List(){
const [values, setValues] = useState({});
const [tabStatus, setStatus] = useStatus(0);
const [pagination, setPagination] = useStatus({
pageNo: 1,
pageSize: 10
});
useEffect(()=>{
query()
},[values, tabStatus, pagination])
return (
<Fragment>
<SearchForm onClick={setValues} />
<Tab
status={status}
onChange={setStatus}
/>
<Pagination
onChange={setPagination}
pageSize={pageSize}
pageNo={pageNo}
/>
</Fragment>
);
}
结语
当然,React Hooks的优点有很多,但是大部分在官方文档和Dan的博文上都有写到,建议大家有情况都去看看,以上只是我通过自己的实践总结出来的一些想法,希望大家一起讨论。