前言
在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
看一下您的程序,您會發現您的組件被各種組件所包裹這叫做包裝地域,比如:providers、comsumers、higher-order components、render props等。這裏有一個更深層的根本問題:React需要一個更好的方法來共享狀態邏輯。
這就是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
感謝閱讀
最後做一個廣告,我創建了一個前端週刊每週五發布最新的技術文章和開源項目歡迎訂閱