React Hook 是 v16.8 的新功能,自誕生以來,受到廣泛的好評,在 React 版本更新中具有里程碑的意義。現在都2020年了,再不上車 React Hook 就真的 out 了...
Hook 動機
本着“存在即合理”的原則,我們先來康康 Hook 為我們解決了哪些問題?Hook 有哪些優勢呢?
在編寫 React 組件時,我們更喜歡函數組件,而不是 class 組件。
因為函數組件代碼更少,結構更清晰,不容易產生 bug。但是,函數組件沒辦法使用狀態,只能作為展示組件(就是個花瓶...哎)。
有了 Hook,我們就能在函數組件中使用狀態了**。毫不誇張的說,以后的組件都可以用函數組件 + Hook 來寫。
class 組件的問題:
- 狀態邏輯難復用:在組件之間復用狀態邏輯很難,一般會用到 render props (渲染屬性)或者 HOC(高階組件)。但無論是渲染屬性,還是高階組件,都會在原先的組件外包裹一層父容器(一般都是 div 元素),導致層級冗余
- 生命周期
- 多而善變的生命周期:用 React 開發的你一定被那些雜亂多變的生命周期惡心過,聲明周期多就不說了,隨着版本的變動會經常變化,導致升級 React 版本時很煩
- 生命周期邏輯混亂:在生命周期函數中混雜不相干的邏輯(如:在
componentDidMount
中注冊事件以及其他的邏輯,在componentWillUnmount
中卸載事件,這樣分散不集中的寫法,很容易寫出 bug ) - class 組件難以拆分:class 組件中到處都是對狀態的訪問和處理,導致組件難以拆分成更小的組件
- this 指向問題:class 組件中的 this 指向問題絕對讓人頭疼,需要我們手動小心翼翼地去綁定 this,一不小心就會出現 bug。
Hook 優勢:
- 優化 class 組件的問題
- 能在無需修改組件結構的情況下復用狀態邏輯(自定義 Hook )
- 能將組件中相互關聯的部分拆分成更小的函數(比如設置訂閱或請求數據)
- 副作用的關注點分離:副作用指那些沒有發生在數據向視圖轉換過程中的邏輯,如
ajax
請求、訪問原生dom
元素、本地持久化緩存、綁定/解綁事件、添加訂閱、設置定時器、記錄日志等。以往這些副作用都是寫在類組件生命周期函數中的。而useEffect
在全部渲染完畢后才會執行,useLayoutEffect
會在瀏覽器layout
之后,painting
之前執行
Hook 規則
Hook 可以讓你在不編寫 class 組件的情況下使用 state 以及其他的 React 特性。但是,有些規則是我們需要准守的:
- 只能在函數內部的最外層調用 Hook
不要在循環,條件或嵌套函數中調用 Hook, 確保總是在你的 React 函數的最頂層調用他們。遵守這條規則,你就能確保 Hook 在每一次渲染中都按照同樣的順序被調用。這讓 React 能夠在多次的useState
和useEffect
調用之間保持 hook 狀態的正確 - 只能在 React 的函數組件(非 class組件)中調用 Hook
不要在普通的 JavaScript 函數或 class 組件中調用 Hook。你可以:- 在 React 的函數組件中調用 Hook
- 在自定義 Hook 中調用其他 Hook
ok,到此為止,我們已經了解 Hook 有哪些優勢了。
下面,我們開始認識最常用的兩個 Hook API—— useState、useEffect。
這兩個API很好理解,而且很實用,弄懂能處理80%的業務場景。本文不會涉及太復雜的操作,僅僅作為入門。
useState
先來看一段簡單的 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 示例進行比較來開始學習 Hook。
等價的 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>
);
}
}
很簡單的一個加1計數器,Hook 寫法比 class 組件是不是簡潔了很多。
下面,我們來分析如何使用 useState Hook...
// 第一步:從 react 庫中引入 useState Hook
import React, { useState } from 'react';
function Example() {
/* 第二步:通過調用 useState Hook 聲明了一個新的 state 變量。
* 它返回一對值(數組)解構到我們命名的變量上。
* 第一個返回的是狀態 count,它存儲的是點擊次數。我們通過傳 0 作為 useState 唯一的參數來將其初始化 0。
* 第二個返回的值本身就是一個函數。它讓我們可以更新 count 的值,所以我們叫它 setCount。
*/
const [count, setCount] = useState(0); // 聲明一個叫 "count" 的 state 變量
return (
<div>
// 第三步:讀取 state,即count
<p>You clicked {count} times</p>
// 第四步:更新 state,通過 setCount()
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
通過上面的分析,我們可以看到使用 useState Hook 管理狀態簡直太爽了。不用寫繁瑣的 class 組件,不用擔心 this 指向,代碼是如此的清晰。
如何使用多個 state 變量:
將 state 變量聲明為一對 [something, setSomething]
也很方便,因為如果我們想使用多個 state 變量,它允許我們給不同的 state 變量取不同的名稱:
function ExampleWithManyStates() {
// 聲明多個 state 變量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '學習 Hook' }]);
...
在以上組件中,我們有局部變量 age
,fruit
和 todos
,並且我們可以單獨更新它們:
function handleOrangeClick() {
// 和 this.setState({ fruit: 'orange' }) 類似
setFruit('orange');
}
你不必使用多個 state 變量。State 變量可以很好地存儲對象和數組,因此,你仍然可以將相關數據分為一組。然而,不像 class 中的 this.setState
,更新 state 變量總是_替換_它而不是合並它。
注意
你可能想知道:為什么叫useState
而不叫createState
?
“Create” 可能不是很准確,因為state 只在組件首次渲染的時候被創建。在下一次重新渲染時,useState
返回給我們當前的 state。否則它就不是 “state”了!這也是 Hook 的名字_總是_以use
開頭的一個原因。
useEffect
Effect Hook定義:useEffect 傳入一個 callback 函數
useEffect(effect: React.EffectCallback, deps?: ReadonlyArray<any> | undefined)
Effect Hook作用:處理函數組件中的副作用,如異步操作、延遲操作等,可以替代Class Component的componentDidMount
、componentDidUpdate
、componentWillUnmount
等生命周期。
Effect Hook特性:
- effect(副作用):指那些沒有發生在數據向視圖轉換過程中的邏輯,如
ajax
請求、訪問原生dom
元素、本地持久化緩存、綁定/解綁事件、添加訂閱、設置定時器、記錄日志等。 - 副作用操作可以分兩類:需要清除的和不需要清除的。
- useEffect 就是一個 Effect Hook,給函數組件增加了操作副作用的能力。它和 class 組件中的
componentDidMount
、componentDidUpdate
和componentWillUnmount
具有相同的用途,只不過被合並成了一個 API - useEffect 接收一個函數,該函數會在組件渲染到屏幕之后才執行。該函數有要求:要么返回一個能清除副作用的函數,要么就不返回任何內容
- 與
componentDidMount
或componentDidUpdate
不同,使用 useEffect 調度的 effect 不會阻塞瀏覽器更新屏幕,這讓你的應用看起來響應更快。大多數情況下,effect 不需要同步地執行。在個別情況下(例如測量布局),有單獨的 useLayoutEffect Hook 供你使用,其 API 與 useEffect 相同
useEffect 使用示例:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 類似 componentDidMount 和 componentDidUpdate
useEffect(() => {
// 使用瀏覽器 API 去更新 document 標題
document.title = `You clicked ${count} times`;
});
// 類似 componentDidMount
useEffect(() => {
// 使用瀏覽器 API 去更新 document 標題
document.title = `You clicked ${count} times`;
}, []); // 慎用!監聽空數組,當 callback 使用到 state 或 props 時最好不要用,因為只能獲取初始化的數據
// 返回一個函數用於清除操作
useEffect(() => {
document.title = `You clicked ${count} times`;
window.addEventListener('load', loadHandle); // loadHandle 函數定義省略
return () => {
window.removeEventListener('load', loadHandle); // 執行清理:callback 下一次執行前調用
};
}, [count]); // 只有當count的值發生變化時,才會重新執行 callback
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect 用法很簡單,但是有兩個地方需要特別注意:
- deps 參數很重要
- useEffect 可以接受第二個參數 deps,用於在 re-render 時判斷是否重新執行 callback
- deps 數組項必須是 immutable 的,比如:不能也不必傳 useRef、dispatch 等
- deps 的比較是淺比較(參閱源碼),傳入對象、函數是無意義
- 作為最佳實踐,使用 useEffect 時請盡可能都傳 deps
- 清除副作用
- useEffect 傳入的 callback 要么返回一個清除副作用的函數,要么什么都不返回。所以,callback 不能用 async 函數(面試題:如何在 useEffect 中使用 async 函數)
- useEffect 傳入的 callback 返回一個函數,在下一次執行 callback 前將會執行這個函數,從而達到清理 effect 的效果
useEffect 的用法大概就是這樣的,有一些坑和更復雜操作這里沒有涉及。當然,要深入理解的話需要去啃源碼了,這里不做過多的解釋。
Hook 核心知識點:閉包
當你在使用 Hook 遇到問題時,請先考慮是否由於閉包引起的。這將幫助你快速排查問題。
總結
Hook 讓我們可以在函數組件中使用狀態state,函數組件一統 React 的時代來了,這很棒。
Hook 可以讓我們摒棄那些繁瑣的生命周期、不用考慮 this 的指向、復用邏輯也不用寫HOC了,這很棒。
Hook 還有更多 API 等着我們去探索,同時也支持自定義 Hook。
Hook 發車啦,用過都說好...
參考:
Hook 官方文檔
30分鍾精通React Hooks
React Hooks完全上手指南