React Hooks 實用指南
前言
在React Conf 2018會議中,Dan Abramov 介紹了 React Hooks。官方的描述為
Hook是一項新功能提案,可讓您在不編寫類的情況下使用狀態和其他React功能。 它們目前處於React v16.7.0-alpha中。計劃將在 2019 Q1 推出到主版本中。
痛點
以下是React Hooks功能的動機,它解決了現有React中的一些問題
元件之間很難共享狀態
React沒有一種將可重用的行為附加到元件的方法(例如連結到store)。如果您使用過一段時間,您可能會使用render props
和height-order components
元件解決這個問題。但是這些模式要求您再使用他們時重構元件,這會很麻煩。如果您使用React DevTools
這就是Hooks
,您可以從元件中提取有狀態邏輯,以便可以獨立測試和重用。Hooks
允許您不更改元件層次結構的情況下重用有狀態邏輯。這樣就可以輕鬆在多元件之間或與社群共享Hooks
。
元件越來越複雜,變得難以理解
我們經常不得不維護一些元件,這些元件一開始很簡單,隨著時間的延伸元件發展成一堆無法管理的有狀態邏輯和一些副作用。每個生命週期方法經常包含不相關的邏輯組合。舉個例子,元件可能會在componentDidMount
componentDidUpdate
中拉取一些資料。還有componentDidMount
方法可能還包含一些事件監聽的不相關邏輯,並且再componentWillUnmount`中解除安裝監聽。但是完全不相關的程式碼會合併到一個方法中。是很容易引起bug和不一致性。
在很多情況下,不能將這些元件拆分成更小的元件因為邏輯遍佈許多地方。對它們進行測試也很困難。這正是很多人將React和狀態管理庫結合使用的原因。但是這更容易建立更多的抽象,要求您在許多不同的檔案之間跳轉,重用元件將變得更加困難。
為了解決這個問題,Hooks
允許您根據相關的功能將他們拆分為一個更小的函式。而不是強制基於宣告周期函式進行拆分。您還可以選擇使用reducer管理元件的本地狀態,使其更具可預測性。
類讓人和機器都混淆
除了使程式碼重用和程式碼組織更加困難外,我們發現類(classes
)可能成為學習React的一大障礙。您必須瞭解它在JavaScript中是如何工作的,這與它在大多數語言中的工作方式有很大不同。您必須明白如何正確的繫結事件處理和還沒穩定的新語法,程式碼非常冗長。大家可能很容易就會明白屬性(props)、狀態(state)、從上往下的資料流(top-down data flow)但類(classes)就很難理解。React中的函式和類元件之間的區別以及何時使用每個元件導致即使在經驗豐富的React開發人員之間也存在分歧。使用函式可以使用prepack更好的優化程式碼。但是使用類元件不能得到更好的優化。
為了解決這些問題,Hooks 允許您在沒有類的情況下使用更多的React功能。
useState
useState
可以讓您的函式元件也具備類元件的state功能
使用語法如下:
const [state, setState] = useState(initialState);
useState
返回一個數組,一個是state的值,第二個是更新state的函式
在真實的程式中我們可以這樣使用:
function TestUseState() {
const [count, setCount] = React.useState(0);
return (
<div>
<p>useState api</p>
<p>Count: {count} <button onClick={() => setCount(count + 1) }>自增</button></p>
</div>
)
}
使用 useState
需要注意一個事項,當你初始化是一個物件時。使用 setCount
時它不像類元件的 this.setState
會自動合併到 state 中。setCount
會使用當前的值覆蓋之前的 state。如下所示
function TestUseStateObject() {
const [state, setState] = React.useState({
count: 0,
greeting: "Hello, World!",
});
const handleAdd = () => {
setState({
count: state.count + 1
})
}
console.log('state > ', state)
return (
<div>
<p>useStateObject api</p>
<p>Count: {state.count} <button onClick={handleAdd}>自增</button></p>
</div>
)
}
我們可以看到,當點選按鈕時 state 被替換成了 {count: 1}。如果想要在 state 中使用一個物件需要在更新值的時候把之前的值解構出來,如下所示:
setState({
...state,
count: state.count + 1
})
在函式中使用多個 state
function TestMultipleUseState() {
const [count, setCount] = React.useState(0);
const [name, setName] = React.useState('john');
return (
<div>
<p>useState api</p>
<p>Count: {count} - Name: {name}</p>
</div>
)
}
如需要線上測試請前往codepen useState
useEffect
預設情況下 useEffect
在完成渲染後執行,我們可以在這裡獲取DOM和處理其他副作用。但它還有兩種不同的執行階段稍候我會解釋。
function TestUseEffect() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log(`元件被更新,Count: ${count}`);
});
return (
<div>
<p>useEffect api</p>
<p>Count: {count} <button onClick={() => setCount(count + 1) }>自增</button></p>
</div>
)
}
上面的 useEffect
在每次元件渲染後執行,每當我們點選自增按鈕都會執行一次。
但是如果上面的程式碼在每次渲染後都執行,如果我們在 useEffect
從伺服器拉取資料。造成的結果就是每次渲染後都會從伺服器拉取資料。或者是隻有某些 props
被更新後才想執行 useEffect
。那麼預設的 useEffect
就不是我們想要執行方式,這時 useEffect
提供了第二個引數。
useEffect(didUpdate, [])
useEffect
第二個引數為一個數組。當我們提供第二個引數時,只有第二個引數被更改 useEffect
才會執行。利用第二個引數我們可以模擬出類元件的 componentDidMount
生命週期函式
function TestUseEffectListener() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log('componentDidMount fetch Data...');
}, []);
return (
<div>
<p>TestUseEffectListener</p>
<p>Count: {count} <button onClick={() => setCount(count + 1) }>自增</button></p>
</div>
)
}
上面的程式碼中 useEffect
只會執行一次,當您點選自增 useEffect
也不會再次執行。
在 useEffect
第一個引數的函式中我們可以返回一個函式用於執行清理功能,它會在ui元件被清理之前執行,結合上面所學的知識使用 useEffect
模擬 componentWillUnmount
生命週期函式
function TestUseEffectUnMount() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
return () => {
console.log('componentUnmount cleanup...');
}
}, []);
return (
<div>
<p>TestUseEffectUnMount</p>
</div>
)
}
上面的程式碼中,當元件 TestUseEffectUnMount
將要銷燬時會,會執行 console.log('componentUnmount cleanup...')
程式碼
如需要線上測試請前往codepen useEffect
useContext
useContext
可以讓您在函式中使用 context
,它有效的解決了以前 Provider
、Consumer
需要額外包裝元件的問題
使用語法如下:
const context = useContext(Context);
現在讓我們來看看實際應用中這個 useContext
是如何使用的,程式碼如下:
function TestFuncContext() {
const context = React.useContext(ThemeContext);
return (
<div style={context}>TestFuncContext</div>
)
}
我們可以看到上面直接使用 React.useContext(ThemeContext)
就可以獲得 context
,而在之前的版本中需要像這樣才能獲取 <Consumer>({vlaue} => {})</Consumer>
,這極大的簡化了程式碼的書寫。
// 之前Consumer的訪問方式
function TestNativeContext() {
return (
<ThemeContext.Consumer>
{(value) => {
return (
<div style={value}>TestNativeContext</div>
)
}}
</ThemeContext.Consumer>
);
}
如需要線上測試請前往codepen useContext
useReducer
useReducer
是 useState
的代提方案。當你有一些更負責的資料時可以使用它。
使用語法如下:
const [state, dispatch] = useReducer(reducer, initialState)
第一個引數是一個 reduce 用來處理到來的 action,函式申明為:(state, action) => ()
。第二個引數是一個初始化的state常量。
在返回值 [state, dispatch]
中,state 就是你的資料。dispatch 可以發起一個 action 到 reducer 中處理。
這個功能給我的感覺就是元件本地的redux,感覺還是不錯。在設計一些複雜的資料結構是可以使用
現在讓我們來看看實際應用中這個 useReducer
是如何使用的,程式碼如下:
function TestUseReducer() {
const [state, setState] = React.useReducer((state, action) => {
switch(action.type) {
case 'update':
return {name: action.payload}
default:
return state;
}
}, {name: ''});
const handleNameChange = (e) => {
setState({type: 'update', payload: e.target.value})
}
return (
<div>
<p>你好:{state.name}</p>
<input onChange={handleNameChange} />
</div>
)
}
當改變 input 中的值時會同時更新 state 中的資料,然後顯示在介面上
如需要線上測試請前往codepen useReducer
useCallback
useCallback
和 useMemo
有些相似。它接收一個行內函數和一個數組,它返回的是一個記憶化版本的函式。
使用語法如下:
const memoizedValue = useMemo(() => computeExpensiveValue(a), [a])
useCallback
的第一個引數是一個函式用來執行一些操作和計算。第二個引數是一個數組,當這個數組裡面的值改變時 useMemo
會重新執行更新這個匿名函式裡面引用到 a
的值。這樣描述可能有點不太好理解,下面看一個例子:
function TestUseCallback({ num }) {
const memoizedCallback = React.useCallback(
() => {
// 一些計算
return num;
},
[],
);
console.log('記憶 num > ', memoizedCallback())
console.log('原始 num > ', num);
return (
<div>
<p>TestUseCallback</p>
</div>
)
}
如果我們想監聽 num
值的更新重新做一些操作和計算,我們可以給第二個引數放入 num
值,像下面這樣:
function TestUseCallback({ num }) {
const memoizedCallback = React.useCallback(
() => {
// 一些計算
return num;
},
[num],
);
console.log('記憶 num > ', memoizedCallback())
console.log('原始 num > ', num);
return (
<div>
<p>TestUseCallback</p>
</div>
)
}
如需要線上測試請前往codepen useCallback
useRef
我覺得 useRef
的功能有點像類屬性,或者說您想要在元件中記錄一些值,並且這些值在稍後可以更改。
使用語法如下:
const refContainer = useRef(initialValue)
useRef
返回一個可變的物件,物件的 current
屬性被初始化為傳遞的引數(initialValue)。返回的物件將持續整個元件的生命週期。
一個儲存input元素,並使其獲取焦點程式,程式碼如下:
function TestUseRef() {
const inputEl = React.useRef(null);
const onButtonClick = () => {
// 點選按鈕會設定input獲取焦點
inputEl.current.focus(); // 設定useRef返回物件的值
};
return (
<div>
<p>TestUseRef</p>
<div>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>input聚焦</button>
</div>
</div>
)
}
useRef
返回的物件您可以在其他地方設定比如: useEffect、useCallback等
如需要線上測試請前往codepen useRef
感謝閱讀
最後做一個廣告,我建立了一個前端週刊每週五發布最新的技術文章和開源專案歡迎訂閱