Why
在介绍React Hooks之前,我们需要了解两个问题:
-
为什么会出现React Hooks
-
它解决了什么问题
我们可以先了解一下以前的React是怎么写的
React.createClass
createClass是一种简单有效的创建组件的方式,最早使用createClass的原因是当时,JavaScript还没有类体系。
React.Component
React v0.13.0 引入了React.Component,它允许我们用JavaScript类来创建组件。
那么,使用中有哪些问题呢?
constructor
当我们使用类创建组件,我们需要constructor方法来初始化组件state,然而,由于我们继承了React.Component,我们需要在每个constructor里都调用super。
constructor (props) {
super(props) // 🤮
...
}
Autobinding
当我们使用createClass,React会自动帮我们bind所有方法到组件实例上。而当我们使用React.Component,我们需要在constructor手动bind,否则就会曝出“Cannot read property setState of undefined”错误。
constructor (props) {
...
this.updateRepos = this.updateRepos.bind(this) // 😭
}
这时我们可能会想:首先,这些问题都很简单,保证调用super(props) 和手动bind虽然繁琐,但是整体没有大问题。第二,这些甚至算不上React的问题,因为JavaScript类就是这么设计的。但是作为程序员,即使最简单的问题,每天重复20+次,也很讨厌。
Class Fields
Class Fields允许我们不用constructor就可以直接给组件类添加实例属性并赋值。这样我们就可以直接初始化state,并且用箭头函数解决bind问题。
class ReposGrid extends React.Component {
state = {
repos: [],
loading: true
}
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos = (id) => {
this.setState({ loading: true })
...
}
...
}
这样就可以了吗?还没。
React的核心思想是让我们把复杂度降解到各个组件,再把组件组合起来,以此来管理App的复杂度。这样的组件模型会让React很优雅。然而,现在的问题不是出在组件模型上,而是出在了模型怎么实现上。
Duplicate Logic
由于历史原因,我们如何构建组件直接和组件生命周期相关。这种生命周期的划分,强制我们把逻辑散布在组件各处。下面的例子中,我们可以清楚的看到,我们需要三个方法(componentDidMount, componentDidUpdate, and updateRepos) 来完成同一件事:保证 repos 和 props.id 同步。
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos = (id) => {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
为了修复这个问题,我们需要新的范式来处理React组件的副作用(side effects)。
Sharing Non-visual Logic
当我们在思考React的组织形式时,我们大概率是以 UI 的组织视角来思考。这很正常,因为 React 对 UI 很在行。
view = fn(state)
现实中,构建一个app仅仅做UI层是不够的。需要复用非可视化的逻辑是一个很常见的需求。然而,由于React把UI耦合在了component上,所以复用非可视化变得很难。从历史上看,React都没有很好的解决这个问题。 结合例子来看,如果我们需要创建另一个也需要repos state的组件。现在,逻辑处理和状态都在ReposGrid组件里。我们怎么复用?最简单的方法是copy所有网络请求和处理数据的逻辑,然后粘贴到新组件。当然这只能是临时方案...更好一点的方案应该是创建一个HOC(Higher-Order Component)来封装所有公共逻辑,并且把 loading 和 repos 作为props传给需要的组件。
function withRepos (Component) {
return class WithRepos extends React.Component {
state = {
repos: [],
loading: true
}
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos = (id) => {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
render () {
return (
<Component
{...this.props}
{...this.state}
/>
)
}
}
}
这样,无论任何组件需要repos(or loading),我们都能用 withRepos HOC来包装。
// ReposGrid.js
function ReposGrid ({ loading, repos }) {
...
}
export default withRepos(ReposGrid)
// Profile.js
function Profile ({ loading, repos }) {
...
}
export default withRepos(Profile)
这个方案可行,而且是推荐的共享非可视逻辑的方案。但是还是有些缺陷。
首先,如果你不熟悉HOC,你会觉得有点跟不上逻辑。以 withRepos HOC为例,这是一个函数,接收一个负责最终渲染的component作为入参,但是返回一个包含我们逻辑的新组件。这个流程很复杂,不易理解。
其次,如果我们需要更多HOC,可以想象,很快我们的代码就不好掌控了。
export default withHover(
withTheme(
withAuth(
withRepos(Profile)
)
)
)
比上面更惨的是最终怎么render。HOC会强迫我们重新构造包装我们的组件。最终会导致“包装地狱”,难以维护。
<WithHover>
<WithTheme hovering={false}>
<WithAuth hovering={false} theme='dark'>
<WithRepos hovering={false} theme='dark' authed={true}>
<Profile
id='JavaScript'
loading={true}
repos={[]}
authed={true}
theme='dark'
hovering={false}
/>
</WithRepos>
</WithAuth>
<WithTheme>
</WithHover>
Current State
现状是:
- React很流行
- 我们用类来写React组件是因为这在当时最合适
- 调用super(props)很烦
- 没人知道this怎么绑定的
- 别急,我们知道你知道this怎么绑定的,但是对其他人是不必要的负担
- 用生命周期来组织组件会让我们把相关的逻辑打散到组件各处
- React没有很好的原始方案来共享非可视逻辑
现在,我们需要一个新的组件方案来解决上述问题,并且保持simple, composable, flexible, and extendable。 这就是React Hooks。