什么是Hook
Hook 是 React 16.8 的新增特性,用途是在函數(shù)組件中使用state肢预、生命周期函數(shù)等其他react特性矛洞,而不用class組件。
Hook 使用規(guī)則
Hook 就是 JavaScript 函數(shù)烫映。
- 只能在函數(shù)最外層調(diào)用 Hook沼本。不要在循環(huán)、條件判斷或者子函數(shù)中調(diào)用锭沟。
- 只能在 React 的函數(shù)組件和自定義的 Hook 中調(diào)用 Hook抽兆。不要在其他 JavaScript 函數(shù)中調(diào)用。
State Hook
在編寫(xiě)函數(shù)組件時(shí)族淮,如果需要添加state辫红,則可以使用state hook,而不用像以前一樣需要改寫(xiě)成class組件祝辣。
Hook 在 class 內(nèi)部是不起作用的贴妻。
useState(),接收一個(gè)參數(shù)state的初始值(數(shù)值蝙斜,字符串名惩,對(duì)象皆可),返回一個(gè)數(shù)組孕荠。數(shù)組的第一個(gè)值是當(dāng)前的state娩鹉,第二個(gè)值是更新state的函數(shù)攻谁。使用數(shù)組解構(gòu)的方式來(lái)接收這兩個(gè)值。一般格式為以下:
// 設(shè)置something初始值為'defaultValue'
const [something, setSomething] = useState('defaultValue')
通過(guò)下面例子底循,可以比較使用hook和class組件不同:
Hook
import React, { useState } from 'react';
function Example() {
// 聲明一個(gè)叫 "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等價(jià)于this.state.count巢株,setCount(count+1)等價(jià)于this.setState({count: count+1})。
使用多個(gè) state 變量
使用多個(gè)state熙涤,只需要多次調(diào)用useState()阁苞。也可以用一個(gè)對(duì)象來(lái)儲(chǔ)存所有的state,但是這樣更新state時(shí)祠挫,需要自己處理其中的合并邏輯(通常使用擴(kuò)展運(yùn)算符...)那槽,具體可參考FAQ中關(guān)于分離獨(dú)立 state 變量的建議。
Effect Hook
Effect Hook 使我們可以在函數(shù)組件中執(zhí)行副作用操作等舔。
- 數(shù)據(jù)獲取骚灸,設(shè)置訂閱以及手動(dòng)更改 React 組件中的 DOM 都屬于副作用。
- useEffect() 可以看做 componentDidMount慌植,componentDidUpdate 和 componentWillUnmount 的組合甚牲。
無(wú)需清除的 effect
在 React 更新 DOM 之后運(yùn)行一些額外的代碼。比如發(fā)送網(wǎng)絡(luò)請(qǐng)求蝶柿,手動(dòng)變更 DOM丈钙,記錄日志等,都是常見(jiàn)的無(wú)需清除的操作交汤。因?yàn)檫@些操作執(zhí)行完之后雏赦,就可以忽略了。
useEffect()
useEffect()接收兩個(gè)參數(shù)芙扎,第一個(gè)參數(shù)是一個(gè)函數(shù)星岗,并且在執(zhí)行 DOM 更新之后調(diào)用這個(gè)函數(shù),第二個(gè)是可選參數(shù)戒洼,是一個(gè)數(shù)組俏橘,如果數(shù)組中的值在兩次重渲染之間沒(méi)有發(fā)生變化,可以通知 React 跳過(guò)對(duì) effect 的調(diào)用施逾。這個(gè)函數(shù)通常稱(chēng)之為effect敷矫,即副作用。使用這個(gè)hook汉额,可以告訴 React 組件需要在渲染后執(zhí)行某些操作。
- 將 useEffect 放在組件內(nèi)部榨汤,使我們可以在 effect 中直接訪(fǎng)問(wèn) state 變量(或其他 props)蠕搜。
- 默認(rèn)情況下,第一次渲染之后和每次更新之后都會(huì)執(zhí)行useEffect收壕。
- 保證了每次運(yùn)行 effect 的同時(shí)妓灌,DOM 都已經(jīng)更新完畢轨蛤,不用再去考慮“掛載”還是“更新”。
- 使用 useEffect 調(diào)度的 effect 不會(huì)阻塞瀏覽器更新屏幕虫埂,這樣使得應(yīng)用看起來(lái)響應(yīng)更快祥山。
通過(guò)下面例子,將 document 的 title 設(shè)置為包含了點(diǎn)擊次數(shù)的消息掉伏,可以比較使用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
有一些副作用是需要清除的缝呕,例如訂閱外部數(shù)據(jù)源。這種情況下斧散,清除工作是非常重要的供常,可以防止引起內(nèi)存泄露。
class組件通常會(huì)在 componentDidMount 中設(shè)置訂閱鸡捐,并在 componentWillUnmount 中清除它栈暇。
通過(guò)下面例子,假設(shè)我們有一個(gè) ChatAPI 模塊箍镜,它允許我們訂閱好友的在線(xiàn)狀態(tài)源祈,可以比較使用hook和class組件不同:
- 如果 effect 返回一個(gè)函數(shù),React 將會(huì)在執(zhí)行清除操作時(shí)調(diào)用它色迂。這是 effect 可選的清除機(jī)制香缺。每個(gè) effect 都可以返回一個(gè)清除函數(shù)。如此可以將添加和移除訂閱的邏輯放在一起脚草。它們都屬于 effect 的一部分赫悄。
- React 會(huì)在組件卸載的時(shí)候執(zhí)行清除操作。
- 并不是必須為 effect 中返回的函數(shù)命名馏慨,也可以返回一個(gè)箭頭函數(shù)埂淮。
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';
}
}
使用多個(gè) Effect 實(shí)現(xiàn)關(guān)注點(diǎn)分離
用 Hook 其中一個(gè)目的就是要解決 class組件 中生命周期函數(shù)經(jīng)常包含不相關(guān)的邏輯,但又把相關(guān)邏輯分離到了幾個(gè)不同方法中的問(wèn)題写隶。就像使用多個(gè) state 的 Hook一樣倔撞,也可以使用多個(gè) effect,將不相關(guān)邏輯分離到不同的 effect 中慕趴。
- Hook 允許按照代碼的用途分離各個(gè)effect痪蝇,而不是像生命周期函數(shù)那樣。
- React 將按照 effect 聲明的順序依次調(diào)用組件中的每一個(gè) effect冕房。
通過(guò)下面例子可以發(fā)現(xiàn)躏啰,在class組件中,設(shè)置 document.title 的邏輯被分割到 componentDidMount 和 componentDidUpdate 中耙册,訂閱邏輯被分割到 componentDidMount 和 componentWillUnmount 中给僵。而且 componentDidMount 中同時(shí)包含了兩個(gè)不同功能的代碼。
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
});
}
// ...
為什么每次更新的時(shí)候都要運(yùn)行 Effect
effect 的清除階段在每次重新渲染時(shí)都會(huì)執(zhí)行,而不是只在卸載組件的時(shí)候執(zhí)行一次帝际。這個(gè)設(shè)計(jì)可以幫助我們創(chuàng)建 bug 更少的組件蔓同。
示例,從 class 中 props 讀取 friend.id蹲诀,然后在組件掛載后訂閱好友的狀態(tài)斑粱,并在卸載組件的時(shí)候取消訂閱:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
但是當(dāng)組件已經(jīng)顯示在屏幕上時(shí),friend prop 發(fā)生變化時(shí)脯爪,這個(gè)組件將繼續(xù)展示原來(lái)的好友狀態(tài)则北。這是一個(gè) bug,而且還會(huì)因?yàn)槿∠嗛啎r(shí)使用錯(cuò)誤的好友 ID 導(dǎo)致內(nèi)存泄露或崩潰的問(wèn)題披粟。內(nèi)存泄露是指當(dāng)一塊內(nèi)存不再被應(yīng)用程序使用的時(shí)候咒锻,由于某種原因,這塊內(nèi)存沒(méi)有返還給操作系統(tǒng)或者內(nèi)存池的現(xiàn)象守屉。
在 class 組件中惑艇,需要添加 componentDidUpdate 來(lái)解決這個(gè)問(wèn)題:
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);
};
});
并不需要特定的代碼來(lái)處理更新邏輯,因?yàn)?useEffect 默認(rèn)就會(huì)處理拇泛。它會(huì)在調(diào)用一個(gè)新的 effect 之前對(duì)前一個(gè) effect 進(jìn)行清理滨巴。例如
// 掛載: { friend: { id: 100 } }
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // 運(yùn)行第一個(gè) effect
// 更新: { friend: { id: 200 } }
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一個(gè) effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // 運(yùn)行下一個(gè) effect
// 更新: { friend: { id: 300 } }
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一個(gè) effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // 運(yùn)行下一個(gè) effect
// 卸載
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一個(gè) effect
此默認(rèn)行為保證了一致性,避免了在 class 組件中因?yàn)闆](méi)有處理更新邏輯而導(dǎo)致常見(jiàn)的 bug俺叭。
通過(guò)跳過(guò) Effect 進(jìn)行性能優(yōu)化
在某些情況下恭取,每次渲染后都執(zhí)行清理或者執(zhí)行 effect 可能會(huì)導(dǎo)致性能問(wèn)題。
在 class 組件中熄守,可以通過(guò)在 componentDidUpdate 中添加對(duì) prevProps 或 prevState 的比較邏輯解決:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
在 useEffect 中蜈垮,如果某些特定值在兩次重渲染之間沒(méi)有發(fā)生變化,可以傳遞數(shù)組作為 useEffect 的第二個(gè)可選參數(shù)裕照,通知 React 跳過(guò)對(duì) effect 的調(diào)用攒发。
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 僅在 count 更改時(shí)更新
對(duì)于有清除操作的 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 發(fā)生變化時(shí),重新訂閱
- 如果要使用此優(yōu)化方式晋南,需確保數(shù)組中包含了所有外部作用域中會(huì)隨時(shí)間變化并且在 effect 中使用的變量惠猿,否則會(huì)引用到先前渲染中的舊變量。
- 如果想執(zhí)行只運(yùn)行一次的 effect(僅在組件掛載和卸載時(shí)執(zhí)行)负间,可以傳遞一個(gè)空數(shù)組([])作為第二個(gè)參數(shù)偶妖。這就告訴 React 你的 effect 不依賴(lài)于 props 或 state 中的任何值,所以它永遠(yuǎn)都不需要重復(fù)執(zhí)行政溃。
Hook規(guī)則
只在最頂層使用 Hook
不要在循環(huán)趾访,條件或嵌套函數(shù)中調(diào)用 Hook, 確倍總是在 React 函數(shù)的最頂層調(diào)用腹缩。遵守這條規(guī)則,能確保 Hook 在每一次渲染中都按照同樣的順序被調(diào)用空扎。這讓 React 能夠在多次的 useState 和 useEffect 調(diào)用之間保持 hook 狀態(tài)的正確藏鹊。如果想要有條件地執(zhí)行一個(gè) effect,可以將判斷放到 Hook 的內(nèi)部转锈。
只在 React 函數(shù)中調(diào)用 Hook
不要在普通的 JavaScript 函數(shù)中調(diào)用 Hook盘寡。
- 在 React 的函數(shù)組件中調(diào)用 Hook。
- 在自定義 Hook 中調(diào)用其他 Hook撮慨。
遵循此規(guī)則竿痰,確保組件的狀態(tài)邏輯在代碼中清晰可見(jiàn)。
可以通過(guò) eslint-plugin-react-hooks 的 ESLint 插件來(lái)強(qiáng)制執(zhí)行這兩條規(guī)則砌溺。
npm install eslint-plugin-react-hooks --save-dev // 安裝
// eslint配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 檢查 Hook 的規(guī)則
"react-hooks/exhaustive-deps": "warn" // 檢查 effect 的依賴(lài)
}
}
自定義 Hook
自定義 Hook 是一個(gè)函數(shù)锡搜,其名稱(chēng)以 “use” 開(kāi)頭讲竿,函數(shù)內(nèi)部可以調(diào)用其他的 Hook。
兩個(gè)函數(shù)之間有共享邏輯時(shí),通常會(huì)把共用部分提取到第三個(gè)函數(shù)中槽华。而組件和 Hook 都是函數(shù),所以也同樣適用這種方式吹榴。
提取自定義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 的頂層無(wú)條件地調(diào)用其他 Hook暖哨,與組件中使用Hook的規(guī)則一致。
- 函數(shù)名字必須以 use 開(kāi)頭培慌,參數(shù)與返回?zé)o要求豁陆,根據(jù)實(shí)際情況自定義
使用自定義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>
);
}