Hooks Api 索引
基礎 Api
useState |
useEffect |
useContext |
---|---|---|
返回一個 state,以及更新 state 的函數。 | 相當于componentDidMount、componentDidUpdate,componentWillUnmount 這三個函數的組合。 | 接收一個 context 對象(React.createContext 的返回值)并返回該 context 的當前值。當前的 context 值由上層組件中距離當前組件最近的 <MyContext.Provider> 的 value prop 決定。 |
額外Api
useReducer |
useCallback |
useMemo |
useRef |
useImperativeHandle |
useLayoutEffect |
useDebugValue |
---|
useState 官方文檔例子
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>
);
}
}
聲明多個state
function ExampleWithManyStates() {
// 聲明多個 state 變量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '學習 Hook' }]);
提示:方括號有什么用?
我們用方括號定義了一個 state 變量
等號左邊名字并不是 React API 的部分,你可以自己取名字
這種 JavaScript 語法叫數組解構。它意味著我們同時創建了fruit
和setFruit
兩個變量,fruit
的值為useState
返回的第一個值,setFruit
是返回的第二個值
const [fruit, setFruit] = useState('banana');
var fruitStateVariable = useState('banana'); // 返回一個有兩個元素的數組
var fruit = fruitStateVariable[0]; // 數組里的第一個值
var setFruit = fruitStateVariable[1]; // 數組里的第二個值
useState除了可以靜態的賦值還可以useState(()=>init)
useState返回的更新數據方法setXXX(currentState=>currentState+1)默認會傳入當前的數據狀態
useEffect
Effect Hook 可以讓你在函數組件中執行副作用操作
解釋這個 Hook 之前先理解下什么是副作用。網絡請求、訂閱某個模塊或者 DOM 操作都是副作用的例子,Effect Hook 是專門用來處理副作用的。正常情況下,在Function Component的函數體中,是不建議寫副作用代碼的,否則容易出 bug。
下面的Class Component例子中,副作用代碼寫在了componentDidMount和componentDidUpdate中:
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>
);
}
}
可以看到componentDidMount和componentDidUpdate中的代碼是一樣的。而使用 Effect 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>
);
}
//useEffect 沒有傳遞依賴參數,所以每次數據更新都會調用useEffect
useEffect
會在每次 DOM 渲染后執行,不會阻塞頁面渲染。它同時具備componentDidMount
、componentDidUpdate
和componentWillUnmount
三個生命周期函數的執行時機
此外還有一些副作用需要組件卸載的時候做一些額外的清理工作的,例如訂閱某個功能:
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';
}
}
在componentDidMount
訂閱后,需要在componentWillUnmount
取消訂閱。使用 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);
// 返回一個函數來進行額外的清理工作:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
當useEffect
的返回值是一個函數的時候,React 會在下一次執行這個副作用之前執行一遍清理工作,整個組件的生命周期流程可以這么理解:
組件掛載 --> 執行副作用 --> 組件更新 --> 執行清理函數 --> 執行副作用 --> 組件更新 --> 執行清理函數 --> 組件卸載
上文提到useEffect
會在每次渲染后執行,但有的情況下我們希望只有在 state
或 props
改變的情況下才執行。如果是Class Component
,我們會這么做:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
使用 Hook
的時候,我們只需要傳入第二個參數:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 只有在 count 改變的時候才執行 Effect
第二個參數是一個數組,可以傳多個值,一般會將 Effect 用到的所有 props 和 state 都傳進去。
當副作用只需要在組件掛載的時候和卸載的時候執行,第二個參數可以傳一個空數組[],實現的效果有點類似componentDidMount
和componentWillUnmount
的組合。
useCallback useMemo
在介紹一下這兩個hooks的作用之前,我們先來回顧一下react中的性能優化。在hooks誕生之前,如果組件包含內部state,我們都是基于class的形式來創建組件。當時我們也知道,react中,性能的優化點在于:
1.調用setState,就會觸發組件的重新渲染,無論前后的state是否不同
2.父組件更新,子組件也會自動的更新
基于上面的兩點,我們通常的解決方案是:使用immutable進行比較,在不相等的時候調用setState;在shouldComponentUpdate中判斷前后的props和state,如果沒有變化,則返回false來阻止更新。
在hooks出來之后,我們能夠使用function的形式來創建包含內部state的組件。但是,使用function的形式,失去了上面的shouldComponentUpdate,我們無法通過判斷前后狀態來決定是否更新。而且,在函數組件中,react不再區分mount和update兩個狀態,這意味著函數組件的每一次調用都會執行其內部的所有邏輯,那么會帶來較大的性能損耗。因此useMemo 和useCallback就是解決性能問題的殺手锏
useCallback
和useMemo
的參數跟useEffect
一致,他們之間最大的區別有是useEffect
會用于處理副作用,而前兩個hooks不能。
useMemo
和useCallback
都會在組件第一次渲染的時候執行
,之后會在其依賴的變量發生改變時再次執行;并且這兩個hooks都返回緩存的值,useMemo
返回緩存的變量
,useCallback
返回緩存的函數
。
useMemo
我們來看一個反例:
import React from 'react';
export default function WithoutMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
function expensive() {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}
return <div>
<h4>{count}-{val}-{expensive()}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</div>;
}
這里創建了兩個state,執行一次昂貴的計算,拿到count對應的某個值。我們可以看到:無論是修改count還是val,由于組件的重新渲染,都會觸發expensive的執行(能夠在控制臺看到,即使修改val,也會打印);但是這里的昂貴計算只依賴于count的值,在val修改的時候,是沒有必要再次計算的。在這種情況下,我們就可以使用useMemo,只在count的值修改時,執行expensive計算:
export default function WithMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
const expensive = useMemo(() => {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}, [count]);
return <div>
<h4>{count}-{expensive}</h4>
{val}
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</div>;
}
上面我們可以看到,使用useMemo來執行昂貴的計算,然后將計算值返回,并且將count作為依賴值傳遞進去。這樣,就只會在count改變的時候觸發expensive執行,在修改val的時候,返回上一次緩存的值。
useCallback
講完了useMemo,接下來是useCallback。useCallback跟useMemo比較類似,但它返回的是緩存的函數。我們看一下最簡單的用法
const fnA = useCallback(fnB, [a])
上面的useCallback會將我們傳遞給它的函數fnB返回,并且將這個結果緩存;當依賴a變更時,會返回新的函數。既然返回的是函數,我們無法很好的判斷返回的函數是否變更,所以我們可以借助ES6新增的數據類型Set來判斷,具體如下:
import React, { useState, useCallback } from 'react';
const set = new Set();
export default function Callback() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
console.log(count);
}, [count]);
set.add(callback);
return <div>
<h4>{count}</h4>
<h4>{set.size}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</div>;
}
我們可以看到,每次修改count,set.size就會+1,這說明useCallback
依賴變量count,count變更時會返回新的函數;而val變更時,set.size不會變,說明返回的是緩存的舊版本函數。
知道useCallback有什么樣的特點,那有什么作用呢?
使用場景是:有一個父組件,其中包含子組件,子組件接收一個函數作為props;通常而言,如果父組件更新了,子組件也會執行更新;但是大多數場景下,更新是沒有必要的,我們可以借助useCallback來返回函數,然后把這個函數作為props傳遞給子組件;這樣,子組件就能避免不必要的更新。
import React, { useState, useCallback, useEffect } from 'react';
function Parent() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
return count;
}, [count]);
return <div>
<h4>{count}</h4>
<Child callback={callback}/>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</div>;
}
function Child({ callback }) {
const [count, setCount] = useState(() => callback());
useEffect(() => {
setCount(callback());
}, [callback]);
return <div>
{count}
</div>
}
useEffect、useMemo、useCallback都是自帶閉包的。也就是說,每一次組件的渲染,其都會捕獲當前組件函數上下文中的狀態(state, props),所以每一次這三種hooks的執行,反映的也都是當前的狀態,你無法使用它們來捕獲上一次的狀態。對于這種情況,我們應該使用ref來訪問。
有空繼續整理