前言
关于Hook的定义官方文档是这么说的:
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
简单来说,就是在使用函数式组件时能用上state,还有一些生命周期函数等其他的特性。
如果想了解Hook怎么用,官方文档和阮一峰的React Hooks 入门教程都讲得很清楚了,我建议直接看官方文档和阮大神的文章即可。
本篇博客只讲为什么要用React的Hook新特性,以及它解决了什么问题。
为什么使用Hook?
让我们先看看别人怎么说。
阮大神的文章中给了一个示例代码:
import React, { Component } from "react";
export default class Button extends Component {
constructor() {
super();
this.state = { buttonText: "Click me, please" };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(() => {
return { buttonText: "Thanks, been clicked!" };
});
}
render() {
const { buttonText } = this.state;
return <button onClick={this.handleClick}>{buttonText}</button>;
}
}
并且提出:
这个组件类仅仅是一个按钮,但可以看到,它的代码已经很"重"了。
真实的 React App 由多个类按照层级,一层层构成,复杂度成倍增长。
再加入 Redux,就变得更复杂。
实际上,上面这个代码的“重”有部分来源于写法问题,他可能并没有“重”,让我们看看下面这种class写法:
import React, { Component } from "react";
export default class Button extends Component {
state = {
buttonText: "Click me, please"
}
handleClick = () => {
this.setState(() => {
return { buttonText: "Thanks, been clicked!" };
});
}
render() {
const { buttonText } = this.state;
return <button onClick={this.handleClick}>{buttonText}</button>;
}
}
然后再对比下使用了Hook的函数式组件:
import React, { useState } from "react";
export default function Button() {
const [buttonText, setButtonText] = useState("Click me, please");
function handleClick() {
return setButtonText("Thanks, been clicked!");
}
return <button onClick={handleClick}>{buttonText}</button>;
}
即使是我们简化过的class写法,比起Hook的看起来好像也确实“重”了点。
Hook的语法确实简练了一些,但是这个理由并不是那么充分。
阮大神同时列举了Redux 的作者 Dan Abramov 总结了组件类的几个缺点:
- 大型组件很难拆分和重构,也很难测试。
- 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。(这里我认为阮大神写的可能有点问题,应该是是各个生命周期方法更为准确)
- 组件类引入了复杂的编程模式,比如 render props 和高阶组件。
这三点都是事实,于是有了函数化的组件,但之前的函数化组件没有state和生命周期,有了Hook那么就可以解决这个痛点。
而且Hook并不只是这么简单,通过自定义Hook,我们可以将原有组件的逻辑提取出来实现复用。
用useEffect解决生命周期导致的重复逻辑或关联逻辑
上面举的几个缺点,第一点和第三点你可能很容易理解,第二点就不容易理解了,所以我们需要深入到具体的代码中去理解这句话。
我们看看下面这段代码:
import React, { Component } from "react";
export default class Match extends Component {
state={
matchInfo:''
}
componentDidMount() {
this.getMatchInfo(this.props.matchId)
}
componentDidUpdate(prevProps) {
if (prevProps.matchId !== this.props.matchId) {
this.getMatchInfo(this.props.matchId)
}
}
getMatchInfo = (matchId) => {
// 请求后台接口获取赛事信息
// ...
this.setState({
matchInfo:serverResult // serverResult是后台接口的返回值
})
}
render() {
const { matchInfo } = this.state
return <div>{matchInfo}</div>;
}
}
这样的代码在我们的业务中经常会出现,通过修改传入赛事组件的ID,去改变这个赛事组件的信息。
在上面的代码中,受生命周期影响,我们需要在加载完毕和Id更新时都写上重复的逻辑和关联逻辑。
所以现在你应该比较好理解这句话:业务逻辑分散在组件的各个生命周期方法之中,导致重复逻辑或关联逻辑。
为了解决这一点,React提供了useEffect这个钩子。
但是在讲这个之前,我们需要先了解到React带来的一个新的思想:同步。
我们在上面的代码中所做的实际上就是在把组件内的状态和组件外的状态进行同步。
所以在使用Hook之前,我们需要先摒弃生命周期的思想,而用同步的思想去思考这个问题。
现在再让我们看看改造后的代码:
import React, { Component } from "react";
export default function Match({matchId}) {
const [ matchInfo, setMatchInfo ] = React.useState('')
React.useEffect(() => {
// 请求后台接口获取赛事信息
// ...
setMatchInfo(serverResult) // serverResult是后台接口的返回值
}, [matchId])
return <div>{matchInfo}</div>;
}
看到这个代码,再对比上面的代码,你心中第一反应应该就是:简单。
React.useEffect接受两个参数,第一个参数是Effect函数,第二个参数是一个数组。
组件加载的时候,执行Effect函数。
组件更新会去判断数组中的各个值是否变动,如果不变,那么不会执行Effect函数。
而如果不传第二个参数,那么无论加载还是更新,都会执行Effect函数。
顺便提一句,这里有组件加载和更新的生命周期的概念了,那么也应该是有组件卸载的概念的:
import React, { Component } from "react";
export default function Match({matchId}) {
const [ matchInfo, setMatchInfo ] = React.useState('')
React.useEffect(() => {
// 请求后台接口获取赛事信息
// ...
setMatchInfo(serverResult) // serverResult是后台接口的返回值
return ()=>{
// 组件卸载后的执行代码
}
}, [matchId])
return <div>{matchInfo}</div>;
}
}
这个常用于事件绑定解绑之类的。
用自定义Hook解决高阶组件
React的高阶组件是用来提炼重复逻辑的组件工厂,简单一点来说就是个函数,输入参数为组件A,输出的是带有某逻辑的组件A+。
回想一下上面的Match组件,假如这个组件是页面A的首页头部用来展示赛事信息,然后现在页面B的侧边栏也需要展示赛事信息。
问题就在于页面A的这块UI需要用div,而页面B侧边栏的这块UI需要用到span。
保证今天早点下班的做法是复制A页面的代码到页面B,然后改下render的UI即可。
保证以后早点下班的做法是使用高阶组件,请看下面的代码:
import React from "react";
function hocMatch(Component) {
return class Match React.Component {
componentDidMount() {
this.getMatchInfo(this.props.matchId)
}
componentDidUpdate(prevProps) {
if (prevProps.matchId !== this.props.matchId) {
this.getMatchInfo(this.props.matchId)
}
}
getMatchInfo = (matchId) => {
// 请求后台接口获取赛事信息
}
render () {
return (
<Component {...this.props} />
)
}
}
}
const MatchDiv=hocMatch(DivUIComponent)
const MatchSpan=hocMatch(SpanUIComponent)
<MatchDiv matchId={1} matchInfo={matchInfo} />
<MatchSpan matchId={1} matchInfo={matchInfo} />
但是实际上有的时候我们的高阶组件可能会更复杂,比如react-redux的connect,这就是高阶组件的复杂化使用方式。
又比如:
hocPage(
hocMatch(
hocDiv(DivComponent)
)
)
毫无疑问高阶组件能让我们复用很多逻辑,但是过于复杂的高阶组件会让之后的维护者望而却步。
而Hook的玩法是使用自定义Hook去提炼这些逻辑,首先看看我们之前使用了Hook的函数式组件:
import React, { Component } from "react";
export default function Match({matchId}) {
const [ matchInfo, setMatchInfo ] = React.useState('')
React.useEffect(() => {
// 请求后台接口获取赛事信息
// ...
setMatchInfo(serverResult) // serverResult是后台接口的返回值
}, [matchId])
return <div>{matchInfo}</div>;
}
然后,自定义Hook:
function useMatch(matchId){
const [ matchInfo, setMatchInfo ] = React.useState('')
React.useEffect(() => {
// 请求后台接口获取赛事信息
// ...
setMatchInfo(serverResult) // serverResult是后台接口的返回值
}, [matchId])
return [matchInfo]
}
接下来,修改原来的Match组件
export default function Match({matchId}) {
const [matchInfo]=useMatch(matchId)
return <div>{matchInfo}</div>;
}
相比高阶组件,自定义Hook更加简单,也更加容易理解。
现在我们再来处理以下这种情况:
hocPage(
hocMatch(
hocDiv(DivComponent)
)
)
我们的代码将不会出现这种不断嵌套情况,而是会变成下面这种:
export default function PageA({matchId}) {
const [pageInfo]=usePage(pageId)
const [matchInfo]=useMatch(matchId)
const [divInfo]=useDiv(divId)
return <ul>
<li>{pageInfo}</li>
<li>{matchInfo}</li>
<li>{divInfo}</li>
</ul>
}
是否需要改造旧的class组件?
现在我们了解到了Hook的好,所以就需要去改造旧的class组件。
官方推荐不需要专门为了hook去改造class组件,并且保证将继续更新class相关功能。
实际上我们也没有必要专门去改造旧项目中的class组件,因为工作量并不小。
但是我们完全可以在新的项目或者新的组件中去使用它。
总结
Hook是对函数式组件的一次增强,使得函数式组件可以做到class组件的state和生命周期。
Hook的语法更加简练易懂,消除了class的生命周期方法导致的重复逻辑代码,解决了高阶组件难以理解和使用困难的问题。
然而Hook并没有让函数式组件能做到class组件做不到的事情,它只是让很多事情变得更加简单而已。
class组件并不会消失,但hook化的函数式组件将是趋势。