Hooks出来快一年了,之前一直持观望态度,了解但没深入过。
最近项目上升级了React Native,借着这个机会重构了一个比较大的页面,比较深入的使用了Hooks,遂有此篇。
本文主观因素较多,如果想系统的学习一下,还是建议去看官网英文文档:https://reactjs.org/docs/hooks-intro.html
第一印象
Hooks是React Team在2018年提出的一种在函数形式的组件中使用state的方法,在此之前我们要使用state的话只能创建一个Class形式的Component。(以下例子大部分来自官网文章)
class Counter extends React.Component {
constructor(props){
super(props);
this.state = {
count: 0
};
}
handleClick = () => {
this.setState({
count: count+1
});
};
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.handleClick}>
Click me
</button>
</div>
);
}
但现在,我们可以用很短几行代码用Hooks实现一遍:
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
可以说是十分Cooooooooooooool了
What Hooks & How Hooks
本文主要介绍两种Hooks:
useState
顾名思义,作用是使用State。
入参是state的初始状态,返回一个数组,第一个元素是state的当前值,第二个元素是state的set方法,你可以用析取把他们取出来顺便起个名字。
当然React并没有对useState做什么黑魔法,所以数组对象和Object对象还是会存在一些关于immutable的问题。
useEffect
看到Effect这个词,了解一点函数式的同学可能会神经一紧。可以参考一下我去年写的文章:https://www.jianshu.com/p/398dd160b2b0
狭义的讲,在组件(可以是一个js意义上的函数,也可以是一个业务意义上的基本单元)运行过程中,修改了外部变量导致组件下一次使用时产生不一样的结果,广义的讲任何涉及组件外部环境的操作,都是副作用。
副作用包括但不限于:
修改一个外部变量;
Call一个API;
打了一个console.log;
所以useEffect也就是使用副作用。
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect稍微复杂一点,它实际上接收两个参数。
useEffect(()=>{}, [])
第一个参数是一个函数,是这个副作用的具体实现。如果涉及到资源的回收的话,这个函数的返回值应当是一个函数,用来包装你的资源回收逻辑。比如你在useEffect里subscribe了某个stream,那么应当在返回函数里面对其进行销毁。
例如
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
第二个参数是一个数组,是这个副作用会被哪些参数trigger。如果不传的话则是会被组件任何props和state的变化trigger。如果传空数组的话则说明不会被任何参数trigger,那么这个effect只会运行初始的一次。
const AdvancedCounter = ({name}) => {
const [count, setCount] = useState(0);
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times, ${name}`;
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
在上面这个例子里,effect只会被count这个state触发。
同时我们应该注意到一点,effect的每次触发都会调一个新的函数。
Why Hooks
1. 用更函数式的方法来使用state
React是一个很函数式的lib,但要使用state这种基础功能的时候却要用class...
2. 有了一种新的方法来封装业务的基本单元
之前的选择很简单,如果是stateful的组件,就用class。如果没有state,就使用function。现在我们基本不用考虑了,用hooks能满足大部分需求。
3. 干掉了生命周期
我不知道有多少人和我一样痛恨React的生命周期方法,虽然某种意义上来讲生命周期也是一种Hooks。
因为生命周期的存在,我们不得不在构思一个组件的一开始就想好它在这个组件生命周期的每个阶段它应该长什么样。
举个例子,现在Counter这个组件有一个需求是要先去后台访问一个get_counter的API来得到现在这个网站的Counter已经被多少个人点过了。
如果使用class形式的component,第一想法是在componentDidMount里面去subscribe这个API,结果放在state里,在componentWillUnmount的里面unsubscribe释放资源。
到目前为止一切还算Nice,简单,可控。
我们的第二个需求来了,我们要在counter能被某一个数整除的时候给现在这个用户弹一个窗口给他一个大大的Surprise,咋整?那在componentDidUpdate里面写吧。
现在还行,组件已经有一点复杂了。
第三个需求:counter能被另一个数字取余等于3的时候给用户弹个窗口再给个Surprise,如果同时满足2和3那让用户的屏幕炸掉。
能做不?
我们想了一下,在componentDidUpdate里面要setState让用户屏幕炸掉,同时不能死循环,要立一个flag,然后还要执行两个不一样的判断。卧槽,复杂度爆表啊。
这个需求砍了吧,没意思。
如果使用Hooks的话,实际上需要4个Hooks。但因为每个Hooks不直接相关,大大简化了复杂度。
4. 抛弃了HOC这种复杂的组件组合方式
想象一下有一个万能的API,我们有些组件需要使用这个API的返回值,有些组件不需要,怎么去share这个stateful的logic?
之前的抽象方法是把这个API封装在一个Component里面,如果组件A需要使用这个API,那么:
const withAPI = (Component) =>
class extends React.Component {
......
}
const A = () => {return <div />};
const AWithAPI = withAPI(A);
如果只有一个可能还好,我们只需要封装这么一层。
如果逻辑很复杂呢?
那么你的代码很可能会变成这样:withAll = (Component) => withA(withB(withC(withD(withWTF(Component)))))
如果使用Hooks,ABCDWTF都可以封装在Hooks里面,某个Component需要使用对应的特性,只需要在function里面call一下就行了。语法以外,这样的Hooks更像一个装饰器。
Thinking in Hooks
使用了Hooks,就不要再被Class的既有经验影响。
总的来说,Hooks是对组合(Composition)这种方法的比较好的实现。
我们在用的时候,也应该摒弃HOC时代的一些旧有想法,不用再关注每层HOC之间的关系。
而应该把目光放在每个Hooks本身承担的业务单元以及该怎么组合这些业务单元来构建一个stateful的component。