什么是Hook
Hook 是 React 16.8 的新增特性,用途是在函数组件中使用state、生命周期函数等其他react特性,而不用class组件。
Hook 使用规则
Hook 就是 JavaScript 函数。
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件和自定义的 Hook 中调用 Hook。不要在其他 JavaScript 函数中调用。
State Hook
在编写函数组件时,如果需要添加state,则可以使用state hook,而不用像以前一样需要改写成class组件。
Hook 在 class 内部是不起作用的。
useState(),接收一个参数state的初始值(数值,字符串,对象皆可),返回一个数组。数组的第一个值是当前的state,第二个值是更新state的函数。使用数组解构的方式来接收这两个值。一般格式为以下:
// 设置something初始值为'defaultValue'
const [something, setSomething] = useState('defaultValue')
通过下面例子,可以比较使用hook和class组件不同:
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>
);
}
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>
);
}
}
可以看出:count等价于this.state.count,setCount(count+1)等价于this.setState({count: count+1})。
使用多个 state 变量
使用多个state,只需要多次调用useState()。也可以用一个对象来储存所有的state,但是这样更新state时,需要自己处理其中的合并逻辑(通常使用扩展运算符...),具体可参考FAQ中关于分离独立 state 变量的建议。
Effect Hook
Effect Hook 使我们可以在函数组件中执行副作用操作。
- 数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。
- useEffect() 可以看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 的组合。
无需清除的 effect
在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志等,都是常见的无需清除的操作。因为这些操作执行完之后,就可以忽略了。
useEffect()
useEffect()接收两个参数,第一个参数是一个函数,并且在执行 DOM 更新之后调用这个函数,第二个是可选参数,是一个数组,如果数组中的值在两次重渲染之间没有发生变化,可以通知 React 跳过对 effect 的调用。这个函数通常称之为effect,即副作用。使用这个hook,可以告诉 React 组件需要在渲染后执行某些操作。
- 将 useEffect 放在组件内部,使我们可以在 effect 中直接访问 state 变量(或其他 props)。
- 默认情况下,第一次渲染之后和每次更新之后都会执行useEffect。
- 保证了每次运行 effect 的同时,DOM 都已经更新完毕,不用再去考虑“挂载”还是“更新”。
- 使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这样使得应用看起来响应更快。
通过下面例子,将 document 的 title 设置为包含了点击次数的消息,可以比较使用hook和class组件不同:
hook
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
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
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>
);
}
}
需要清除的 effect
有一些副作用是需要清除的,例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露。
class组件通常会在 componentDidMount 中设置订阅,并在 componentWillUnmount 中清除它。
通过下面例子,假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态,可以比较使用hook和class组件不同:
- 如果 effect 返回一个函数,React 将会在执行清除操作时调用它。这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
- React 会在组件卸载的时候执行清除操作。
- 并不是必须为 effect 中返回的函数命名,也可以返回一个箭头函数。
hook
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
class
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
使用多个 Effect 实现关注点分离
用 Hook 其中一个目的就是要解决 class组件 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。就像使用多个 state 的 Hook一样,也可以使用多个 effect,将不相关逻辑分离到不同的 effect 中。
- Hook 允许按照代码的用途分离各个effect,而不是像生命周期函数那样。
- React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
通过下面例子可以发现,在class组件中,设置 document.title 的逻辑被分割到 componentDidMount 和 componentDidUpdate 中,订阅逻辑被分割到 componentDidMount 和 componentWillUnmount 中。而且 componentDidMount 中同时包含了两个不同功能的代码。
hook
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
class
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...
为什么每次更新的时候都要运行 Effect
effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。这个设计可以帮助我们创建 bug 更少的组件。
示例,从 class 中 props 读取 friend.id,然后在组件挂载后订阅好友的状态,并在卸载组件的时候取消订阅:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
但是当组件已经显示在屏幕上时,friend prop 发生变化时,这个组件将继续展示原来的好友状态。这是一个 bug,而且还会因为取消订阅时使用错误的好友 ID 导致内存泄露或崩溃的问题。内存泄露是指当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者内存池的现象。
在 class 组件中,需要添加 componentDidUpdate 来解决这个问题:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// 取消订阅之前的 friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// 订阅新的 friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
hook
function FriendStatus(props) {
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
并不需要特定的代码来处理更新逻辑,因为 useEffect 默认就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。例如
// 挂载: { friend: { id: 100 } }
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // 运行第一个 effect
// 更新: { friend: { id: 200 } }
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // 运行下一个 effect
// 更新: { friend: { id: 300 } }
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // 运行下一个 effect
// 卸载
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect
此默认行为保证了一致性,避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。
通过跳过 Effect 进行性能优化
在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。
在 class 组件中,可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
在 useEffect 中,如果某些特定值在两次重渲染之间没有发生变化,可以传递数组作为 useEffect 的第二个可选参数,通知 React 跳过对 effect 的调用。
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
对于有清除操作的 effect 同样适用:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
- 如果要使用此优化方式,需确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则会引用到先前渲染中的旧变量。
- 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。
Hook规则
只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在 React 函数的最顶层调用。遵守这条规则,能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。如果想要有条件地执行一个 effect,可以将判断放到 Hook 的内部。
只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。
- 在 React 的函数组件中调用 Hook。
- 在自定义 Hook 中调用其他 Hook。
遵循此规则,确保组件的状态逻辑在代码中清晰可见。
可以通过 eslint-plugin-react-hooks 的 ESLint 插件来强制执行这两条规则。
npm install eslint-plugin-react-hooks --save-dev // 安装
// eslint配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
"react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
}
}
自定义 Hook
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
两个函数之间有共享逻辑时,通常会把共用部分提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。
提取自定义Hook
useFriendStatus 自定义Hook
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
- 确保只在自定义 Hook 的顶层无条件地调用其他 Hook,与组件中使用Hook的规则一致。
- 函数名字必须以 use 开头,参数与返回无要求,根据实际情况自定义
使用自定义Hook
在 FriendStatus 和 FriendListItem 组件中使用自定义的useFriendStatus
Hook。
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
网友评论