Hook簡介
React Hooks是React 16.7.0-alpha版本推出的新特性,目的是解決React的狀態共享問題。稱之為狀態共享可能描述的並不是很恰當,稱為狀態邏輯複用可能會更恰當,因為React Hooks只共享資料處理邏輯,並不會共享資料本身。 在React應用開發中,狀態管理是元件開發必不可少的內容。以前,為了對狀態進行管理,最通常的做法是使用類元件或者直接使用redux等狀態管理框架。例如:
class Hook 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>
);
}
}
複製程式碼
現在,開發者可以直接使用React Hooks提供的State Hook來處理狀態,針對那些已經存在的類元件,也可以使用State Hook很好地進行重構。例如:
import React, { useState } from 'react';
function Hook() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
複製程式碼
從上面的示例可以發現,Example變成了一個函式元件,此函式元件有自己的狀態,並且還可以更新自己的狀態。之所以可以如此操作,是因為使用了useState,useState是react自帶的一個hook函式,它的作用就是用來宣告狀態變數。
Hook API
一直以來,React就在致力於解決一個問題,即狀態元件的複用問題。在React應用開發中,我們都是通過元件和自上而下傳遞的資料流來將大型的檢視拆分成獨立的可複用元件。但是在實際專案開發中,複用一個帶有業務邏輯的元件依然是一個難題。 眾所周知,React提供了兩種建立元件的方式,即函式元件和類元件。函式元件是一個普通的JavaScript函式,接受props物件並返回React元素,函式元件更符合React資料驅動檢視的開發思想。不過,函式元件一直以來都因為缺乏類元件諸如狀態、生命週期等種種特性,也因為這些原因函式元件得不到開發者的青睞,而Hooks的出現就是讓函式式元件擁有類元件的特性。 為了讓函式元件擁有類元件的諸如狀態、生命週期等特性,React 提供了3個核心的api,即State Hooks、Effect Hooks和Custom Hooks。 useState就是React提供最基礎、最常用的Hook,主要用來定義和管理本地狀態。例如,下面是使用useState實現一個最簡單的計數器,程式碼如下:
import React, { useState } from 'react'
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={()=> setCount(count + 1)}>+</button>
<span>{count}</span>
<button onClick={() => setCount((count) => count - 1)}>-</button>
</div>
);
}
export default App;
複製程式碼
在上面的示例中,使用useState來定義一個狀態,與類元件的狀態不同,函式元件的狀態可以是物件也可以是基礎型別值。useState返回的是一個陣列,陣列的第一個物件表示當前狀態的值,第二個物件表示用於更改狀態的函式,類似於類元件的setState。 函式元件中如果存在多個狀態,既可以通過一個useState宣告物件型別的狀態,也可以通過useState多次宣告狀態。例如:
//宣告物件型別狀態
const [count, setCount] = useState({
count1: 0,
count2: 0
});
//多次宣告
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
複製程式碼
可以發現,相比於宣告物件型別狀態,多次宣告狀態的方式更加方便,主要是因為更新函式是採用的替換的方式而不是合併的方式。 不過,如果要在函式元件中處理多層巢狀資料邏輯時,使用useState就顯得力不從心了。值得慶幸的是,開發者可以使用React提供的useReducer來處理此類問題。例如:
import React, {useReducer} from 'react'
const reducer = function (state, action) {
switch (action.type) {
case "increment":
return {count: state.count + 1};
case "decrement":
return {count: state.count - 1};
default:
return {count: state.count}
}
};
function Example() {
const [state, dispatch] = useReducer(reducer, {count: 0});
const {count} = state;
return (
<div>
<button onClick={() => dispatch({type: "increment"})}>+</button>
<span>{count}</span>
<button onClick={() => dispatch({type: "decrement"})}>-</button>
</div>
);
}
export default Example;
複製程式碼
可以發現,useReducer接受reducer函式和預設值兩個引數,並返回當前狀態state和dispatch函式的陣列,使用方式與Redux基本一致,不同之處在於,Redux的預設值是通過給reducer函式設定預設引數的方式給定的。 useReducer之所以沒有采用Redux的方式設定預設值,是因為React認為狀態的的預設值可能是來自於函式元件的props。例如:
function Example({initialState = 0}) {
const [state, dispatch] = useReducer(reducer, { count: initialState });
...
}
複製程式碼
解決了函式元件中內部狀態的問題,接下來亟待解決的就是函式元件中生命週期函式的問題。在React的函數語言程式設計思想中,生命週期函式是溝通函式式和命令式的橋樑,根據生命週期函式開發者可以執行相關的操作,如網路請求和操作DOM。
import React, {useState, useEffect} from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('componentDidMount...')
console.log('componentDidUpdate...')
return () => {
console.log('componentWillUnmount...')
}
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Example;
複製程式碼
執行上面的程式碼,然後點選按鈕執行加法操作時,生命週期函式呼叫情況下圖所示。
可以看到,每次執行元件更新時useEffect中的回撥函式都會被呼叫。並且在點選按鈕執行更新操作時還會觸發componentWillUnmount生命週期,之所以在重新繪製前執行銷燬操作,是為了避免造成記憶體洩露。 因此可以將useEffect視為componentDidMount、componentDidUpdate和componentWillUnmount的組合,並用它關聯函式元件的生命週期。 需要說明是,類元件的componentDidMount或componentDidUpdate生命週期函式都是在DOM更新後同步執行的,但useEffect並不會在DOM更新後同步執行。因此,使用useEffect的並不會阻塞瀏覽器更新介面。如果需要模擬生命週期的同步執行,可以使用React提供的useLayoutEffect Hook。自定義Hook
眾所周知,要在類元件之間共享一些狀態邏輯是非常麻煩的,常規的做法都是通過高階元件或者是函式的屬性來解決。不過,新版的React允許開發者建立自定義Hook來封裝共享狀態邏輯,且不需要向元件樹中增加新的元件。 所謂的自定義Hook,其實就是指函式名以use開頭並呼叫其他Hook的函式,自定義Hook的每個狀態都是完全獨立的。例如,下面是使用axios實現網路請求的示例,程式碼如下:
import axios from 'axios'
export const useAxios = (url, dependencies) => {
const [isLoading, setIsLoading] = useState(false);
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
setIsLoading(true);
axios.get(url).then((res) => {
setIsLoading(false);
setResponse(res);
}).catch((err) => {
setIsLoading(false);
setError(err);
});
}, dependencies);
return [isLoading, response, error];
};
複製程式碼
如上所示,就是使用axios開發請求資料的自定義Hook。使用方法和系統提供的Hook類似,直接呼叫即可。例如:
function Example() {
let url = 'http://api.douban.com/v2/movie/in_theaters';
const [isLoading, response, error] = useAxios(url, []);
return (
<div>
{isLoading ? <div>loading...</div> :
(error ? <div> There is an error happened </div> : <div> Success, {response} </div>)}
</div>
)
}
export default Example;
複製程式碼
可以發現,相比於函式屬性和高階元件等方式,自定義Hook則更加的簡潔易讀,不僅於此,自定義Hook也不會引起之元件巢狀地獄問題。 雖然React的Hooks有著諸多的優勢。不過,在使用Hooks的過程中,需要注意以下兩點: • 不要在迴圈、條件或巢狀函式中使用Hook,並且只能在React函式的頂層使用Hook。之所以要這麼做,是因為React需要利用呼叫順序來正確更新相應的狀態,以及呼叫相應的生命週期函式函式。一旦在迴圈或條件分支語句中呼叫Hook,就容易導致呼叫順序的不一致性,從而產生難以預料到的後果。 • 只能在React函式式元件或自定義Hook中使用Hook。 同時,為了避免在開發中造成一些低階的錯誤,可以安裝一個eslint外掛,安裝命令如下:
yarn add eslint-plugin-react-hooks --dev
複製程式碼
然後,在eslint的配置檔案中新增如下一些配置。
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
複製程式碼
藉助於React提供的Hooks API,函式元件可以實現絕大部分的類元件功能,並且Hooks在共享狀態邏輯、提高元件複用性上也具有一定的優勢。可以預見的是,Hooks將是React未來發展的重要方向。