一、Hook 的简介
什么是 Hook?
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及生命周期等特性的函数。
为什么会有 Hook?
-
在组件之间复用状态逻辑很难 React 提供了一些方法来实现组件的选择性渲染或复用,如 Render Props、高阶组件 等,但这些方法组成的组件也带来了“嵌套地狱”的问题,复杂了组件的结构,增大了阅读理解代码的难度。React 提供了自定义 Hook 来解决上面提到的问题。
-
复杂组件变得难以理解 在一些复杂的组件中往往都包含了较多的状态逻辑和方法,如组件常常在 componentDidMount 中获取数据。但是,同时在该生命周期中也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。一些原本没关联的的代码因为修改了相同的状态而冗杂在一起。当然 React 也引入了 Redux 用来实现状态管理,但这也引入了很多抽象概念,文件也相对应复杂起来。Hook 将组件中相互关联的部分拆分成更小的函数,而并非强制按照生命周期划分。
-
难以理解的 class 在组件中使用 class 时,开发者必须去理解 JavaScript 中 this 的工作方式,需要处理事件绑定等等,另外,class 还存在不能很好的压缩等问题。Hook 使你在非 class 的情况下可以使用更多的 React 特性。
什么时候用 Hook?
在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。
二、Hook 的使用
State Hook
useState 是一个 State Hook ,用来增加组件中的 state ,下面分别通过 class 和 useState 两种写法实现一个计数器。
class 实现方式:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
state 初始值为 { count: 0 } ,当点击按钮后,通过调用 this.setState() 来增加 state.count。
Hook 实现方式:
import React, { useState } from "react";
function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
在组件中调用 useState Hook,定义一个 “state 变量”。自定义变量 count,它与 class 里面的 this.state 提供的功能完全相同。useState() 方法里面唯一的参数就是初始 state ,此处传值为 0 ,它返回值为一个数组,包括了当前 state 以及更新 state 的函数的,分别对应这里的 count 和 setCount 。 如果需要使用多个 state 变量只需要执行对应数量的 useState Hook 即可。
Effect Hook
在 React 组件中执行数据获取、订阅或者手动修改过 DOM,这些操作称为“副作用”,简称为“作用”。
useEffect 一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。
当使用 useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行“副作用”函数。由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数,包括第一次渲染的时候。
下面为上面的计数器增加了一个小功能:将 document 的 title 设置为包含了点击次数的消息。
class 的实现:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
Hook 实现方式:
import React, { useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
// 作用同 componentDidMount and componentDidUpdate:
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
通过上面两种实现方式,不难发现,在 class 中,如果我们想在组件加载和更新时执行同样的操作,需要在两个生命周期函数中编写重复的代码。
而使用 Hook 则不再需要考虑组件此时是“挂载”还是“更新”,默认情况下,useEffect 在第一次渲染之后和每次更新之后都会执行。在组件内部中使用 useEffect 时,需要传一个函数 “effect” ,在函数中可直接访问 state 或 props,然后告诉 React 组件需要在渲染后执行某些操作。useEffect 也可接受第二个参数
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
这个参数可通过跳过 Effect 进行性能优化,传入的 [count] 会和前一次渲染的 count 进行对比,如果相同则跳过本次 effct,作用类似于在 componentDidUpdate 中添加对 prevProps 或 prevState。
Hook 也是允许我们清除 effect ,类似生命周期 componentWillUnmount ,下面为我们的计数器增加一个定时器,并且在组件卸载的时候清楚这个定时器。
class 实现方式:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
let _this = this;
_this.interval = setInterval(() => {
_this.setState({
count: count++
});
}, 1000);
document.title = `count is ${this.state.count}`;
}
componentDidUpdate() {
document.title = `count is ${this.state.count}`;
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return (
<div>
<p>count is {this.state.count}</p>
</div>
);
}
}
Hook 实现方式:
import React, { useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
let interval = null;
useEffect(() => {
document.title = `count is ${count}`;
interval = setInterval(() => {
setCount(count + 1);
}, 1000);
//若存在,则清除 effect 操作
return clearInterval(interval);
});
return (
<div>
<p>count is {count}</p>
</div>
);
}
在 componentDidMount 和 componentWillUnmount 之间是相互对应的,两者作用于相同的副作用,即开启定时器和清除定时器,但此时两者的逻辑是拆分开来写的。
在 Hook 中,清除 effect 是设计在同一个地方执行的,当 effect 返回一个函数,React 将会在执行清除操作时调用它,即在卸载组件时执行 clearInterval ,保证了同一个 effect 的逻辑可以放在一起。
自定义 Hook
通过自定义 Hook,可以将组件公共逻辑提取到可重用的函数中。
前面我们给计数器加了定时器,每秒+1,如果我们想按每秒+2 或者其他规律修改 count 的话,我们可以修改计算规则,这里举例+2:
import React, { useState, useEffect } from "react";
function ExampleB() {
const [count, setCount] = useState(0);
let interval = null;
useEffect(() => {
document.title = `count is ${count}`;
interval = setInterval(() => {
setCount(count + 2);
}, 1000);
//若存在,则清除 effect 操作
return clearInterval(interval);
});
return (
<div>
<p>count is {count}</p>
</div>
);
}
很明显,这里只是修改了一个地方,两个组件有公共的逻辑,因此我们可以使用自定义 Hook 将公共的部分抽离出来,Hook 是可以让我们不增加组件的情况下解决相同问题。
提取自定义 Hook
自定义 Hook 其实就是个内部使用了 useState 、useEffect 的普通函数,其名称以 “use” 开头,可以将组件逻辑提取到可重用的函数中;
import React, { useState, useEffect } from "react";
function useSetCount(c,n) {
const [count, setCount] = useState(c);
let interval = null;
useEffect(() => {
interval = setInterval(() => {
setCount(count + n);
}, 1000);
return clearInterval(interval);
});
return count;
}
与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use 开头。 此处自定义了一个 useSetCount Hook,接受 count 和计算数值,并返回计算结果。
使用自定义 Hook
我们可以在需要的组件中使用自定义的 useSetCount ,比如:
function ExampleA(props) {
const count = useSetCount(1, 1);
return (
<div>
<p>count is {count}</p>
</div>
);
}
function ExampleB(props) {
const count = useSetCount(10, 10);
return (
<div>
<p>count is {count}</p>
</div>
);
}
注意:
- 自定义 Hook 必须以 “use” 开头。 如果不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
- 两个组件中使用相同的 Hook 不会共享 state 自定义 Hook 是一种重用状态逻辑的机制,所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
三、Hook 规则
Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则:
- 只在最顶层使用 Hook 不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
- 只在 React 函数中调用 Hook 不要在普通的 JavaScript 函数中调用 Hook ,遵循此规则,确保组件的状态逻辑在代码中清晰可见。