爲什麼要用 Hooks
組件嵌套問題
之前如果我們需要抽離一些重複的邏輯,就會選擇 HOC
或者 render props
的方式。但是通過這樣的方式去實現組件,你打開 React DevTools
就會發現組件被各種其他組件包裹在裏面。這種方式首先提高了 debug
的難度,並且也很難實現共享狀態。
但是通過 Hooks
的方式去抽離重複邏輯的話,一是不會增加組件的嵌套,二是可以實現狀態的共享。
class 組件的問題
如果我們需要一個管理狀態的組件,那麼就必須使用 class
的方式去創建一個組件。但是一旦 class
組件變得複雜,那麼四散的代碼就很不容易維護。另外 class
組件通過 Babel
編譯出來的代碼也相比函數組件多得多。
Hooks
能夠讓我們通過函數組件的方式去管理狀態,並且也能將四散的業務邏輯寫成一個個 Hooks
便於複用以及維護。
Hooks 怎麼用
前面說了一些 Hooks 的好處,接下來我們就進入正題,通過實現一個計數器來學習幾個常用的 Hooks。
useState
useState
的用法很簡單,傳入一個初始 state
,返回一個 state
以及修改 state
的函數。
// useState 返回的 state 是個常量
// 每次組件重新渲染之後,當前 state 和之前的 state 都不相同
// 即使這個 state 是個對象
const [count, setCount] = useState(1)
setCount
用法是和 setState
一樣的,可以傳入一個新的狀態或者函數。
setCount(2)
setCount(prevCount => prevCount + 1)
useState
的用法是不是很簡單。假如現在需要我們實現一個計數器,按照之前的方式只能通過 class
的方式去寫,但是現在我們可以通過函數組件 + Hooks
的方式去實現這個功能。
function Counter() {
const [count, setCount] = React.useState(0)
return (
<div>
Count: {count}
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
</div>
);
}
useEffect
現在我們的計時器需求又升級了,需要在組件更新以後打印出當前的計數,這時候我們可以通過 useEffect
來實現
function Counter() {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
console.log(count)
})
return (
<div>
Count: {count}
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
</div>
);
}
以上代碼當我們改變計數的時候,就會打印出正確的計數,我們其實基本可以把 useEffect
看成是 componentDidUpdate
,它們的區別我們可以在下一個例子中看到。
另外 useEffect
還可以返回一個函數,功能類似於 componentWillUnmount
。
function Counter() {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
console.log(count)
return () => console.log('clean', count)
})
// ...
}
當我們每次更新計數時,都會先打印 clean
這行 log
現在我們的需求再次升級了,需要我們在計數器更新以後延時兩秒打印出計數。實現這個再簡單不過了,我們改造下 useEffect
內部的代碼即可
React.useEffect(() => {
setTimeout(() => {
console.log(count)
}, 2000)
})
當我們快速點擊按鈕後,可以在兩秒延時以後看到正確的計數。但是如果我們將這段代碼寫到 componentDidUpdate
中,事情就變得不一樣了。
componentDidUpdate() {
setTimeout(() => {
console.log(this.state.count)
}, 2000)
}
對於這段代碼來說,如果我們快速點擊按鈕,你會在延時兩秒後看到打印出了相同的幾個計數。這是因爲在 useEffect
中我們通過閉包的方式每次都捕獲到了正確的計數。但是在 componentDidUpdate
中,通過 this.state.count
的方式只能拿到最新的狀態,因爲這是一個對象。
當然如果你只想拿到最新的 state
的話,你可以使用 useRef
來實現。
function Counter() {
const [count, setCount] = React.useState(0)
const ref = React.useRef(count)
React.useEffect(() => {
ref.current = count
setTimeout(() => {
console.log(ref.current)
}, 2000)
})
//...
}
useRef
可以用來存儲任何會改變的值,解決了在函數組件上不能通過實例去存儲數據的問題。另外你還可以 useRef
來訪問到改變之前的數據。
function Counter() {
const [count, setCount] = React.useState(0)
const ref = React.useRef()
React.useEffect(() => {
// 可以在重新賦值之前判斷先前存儲的數據和當前數據的區別
ref.current = count
})
<div>
Count: {count}
PreCount: {ref.current}
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
</div>
//...
}
現在需求再次升級,我們需要通過接口來獲取初始計數,我們通過 setTimeout
來模擬這個行爲。
function Counter() {
const [count, setCount] = React.useState();
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
setLoading(true);
setTimeout(() => {
setCount(1);
setLoading(false);
}, 2000);
});
return (
<div>
{!loading ? (
<div>
Count: {count}
<button onClick={() => setCount(pre => pre + 1)}>+</button>
<button onClick={() => setCount(pre => pre - 1)}>-</button>
</div>
) : (
<div>loading</div>
)}
</div>
);
}
如果你去執行這段代碼,會發現 useEffect
無限執行。這是因爲在 useEffect
內部再次觸發了狀態更新,因此 useEffect
會再次執行。
解決這個問題我們可以通過 useEffect
的第二個參數解決
React.useEffect(() => {
setLoading(true);
setTimeout(() => {
setCount(1);
setLoading(false);
}, 2000);
}, []);
第二個參數傳入一個依賴數組,只有依賴的屬性變更了,纔會再次觸發 useEffect
的執行。在上述例子中,我們傳入一個空數組就代表這個 useEffect
只會執行一次。
現在我們的代碼有點醜陋了,可以將請求的這部分代碼單獨抽離成一個函數,你可能會這樣寫
const fetch = () => {
setLoading(true);
setTimeout(() => {
setCount(1);
setLoading(false);
}, 2000);
}
React.useEffect(() => {
fetch()
}, [fetch]);
但是這段代碼出現的問題和一開始的是一樣的,還是會無限執行。這是因爲雖然你傳入了依賴,但是每次組件更新的時候 fetch
都會重新創建,因此 useEffect
認爲依賴已經更新了,所以再次執行回調。
解決這個問題我們需要使用到一個新的 Hooks useCallback
。這個 Hooks
可以生成一個不隨着組件更新而再次創建的 callback
,接下來我們通過這個 Hooks
再次改造下代碼
const fetch = React.useCallback(() => {
setLoading(true);
setTimeout(() => {
setCount(1);
setLoading(false);
}, 2000);
}, [])
React.useEffect(() => {
fetch()
}, [fetch]);
大功告成,我們已經通過幾個 Hooks +
函數組件完美實現了原本需要 class
組件才能完成的事情。
總結
通過幾個計數器的需求我們學習了一些常用的 Hooks
,接下來總結一下這部分的內容。
-
useState
:傳入我們所需的初始狀態,返回一個常量狀態以及改變狀態的函數 -
useEffect
:第一個參數接受一個callback
,每次組件更新都會執行這個callback
,並且callback
可以返回一個函數,該函數會在每次組件銷燬前執行。如果useEffect
內部有依賴外部的屬性,並且希望依賴屬性不改變就不重複執行useEffect
的話,可以傳入一個依賴數組作爲第二個參數 -
useRef
:如果你需要有一個地方來存儲變化的數據 -
useCallback
:如果你需要一個不會隨着組件更新而重新創建的callback