在react conf 2018上,react釋出了一個新的提案hook。穩定的正式版可能要等一兩個月之後才能出來,目前可以在v16.7.0-alpha上試用到rfc上各種提問。
那麼這個hook到底是個什麼呢,官方的定義是這樣的
Hooks are a new feature proposal that lets you use state and other React features without writing a class.
這是一個比class更直觀的新寫法,在這個寫法中react元件都是純函式,沒有生命週期函式,但可以像class一樣擁有state,可以由effect觸發生命週期更新,提供一種新的思路來寫react。(雖然官方再三宣告我們絕對沒有要拿掉class的意思,但hook未來的目標是覆蓋所有class的應用場景)
其實在看demo演示的時候我是十分抗拒的,沒有生命週期函式的react是個什麼黑魔法,雖然程式碼變得乾淨了不少,但寫法實在是發生了很大的轉變,有種脫離掌控的不安全感,我甚至有點懷疑我能不能好好debug。
演示的最後dan的結束語是這樣的
hook代表了我們對react未來的願景,也是我們用來推動react前進的方法。因此我們不會做大幅的重寫,我們會讓舊的class模式和新的hook模式共存,所以我們可以一起慢慢的接納這個新的react。
我接觸react已經四年了,第一次接觸它的時候,我第一個想問的是,為什麼要用jsx。第二個想問的是,為什麼要用這個logo,畢竟我們又不是叫atom,也不是什麼物理引擎。現在我想到了了一個解釋,原子的型別和屬性決定了事物的外觀和表現,react也是一樣的,你可以把介面劃分為一個個獨立的元件,這些元件(component)的型別(type)和屬性(props)決定了最終介面的外觀和表現。諷刺的是,原子一直被認為是不可分的,所以當科學家第一次發現原子的時候認為這就是最小的單元,直到後來在原子中發現了電子,實際上電子的運動更能決定原子能做什麼。hook也是一樣的,我不認為hook是一個新的react特性,相反的,我認為hook能讓我更直觀的瞭解react的基本特性像是state、context、生命週期。hook能更直觀的代表react,它解釋了元件內部是如何工作的,我認為它被遺落了四年,當你看到react的logo,可以看到電子一直環繞在那裡,hook也是,它一直在這裡。
於是我決定乾了這杯安利。
試了幾個比較基本的api寫了幾個demo,程式碼在 github.com/lllbahol/re…, 完全的api還請參考官方文件 reactjs.org/docs/hooks-…
api
基本的hook有三個
- useState(相當於state)
- useEffect(相當於componentDidUpdate, componentDidMount, componentWillUnmount)
- useContext(相當於Context api)
useState
const [state, setState] = useState(initialState);
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
複製程式碼
在這裡react元件就是一個簡單的function
-
useState(initialState)也是一個函式,定義了一個state,初始值為initialState,返回值是一個陣列,0為state的值,1為setState的方法。
-
當state發生變化時,函式元件重新整理。
-
可以useState多次來定義多個state,react會根據呼叫順序來判斷。
你一定也寫過一個龐大的class, 有一堆handler函式,因為要setState所以不能挪到元件外面去,然後render函式就被擠出了頁面,每次想看render都要把頁面滾到底下。
現在因為useState是函式,所以它可以被挪到元件外面,連帶handler一起,下面是一個具體一點的表單例子。
import React, { useState } from 'react';
// 表單元件,有name, phone兩個輸入框。
export default () => {
const name = useSetValue('hello');
const phone = useSetValue('120');
return (
<React.Fragment>
<Item {...name} />
<br />
<Item {...phone} />
</React.Fragment>
);
}
// controlled input component
const Item = ({ value, setValue }) => (
<React.Fragment>
<label>{value}</label>
<br />
<input value={value} onChange={setValue} />
</React.Fragment>
);
// 可以將state連同handler function一起挪到元件外面。
// 甚至可以export出去,讓其他元件也能使用這個state邏輯
const useSetValue = (initvalue) => {
const [value, setValue] = useState(initvalue);
const handleChange = (e) => {
setValue(e.target.value);
}
return {
value,
setValue: handleChange,
};
}
複製程式碼
useEffect
這個api可以讓你在函式元件中使用副作用(use side effects),常見的會產生副作用的方式有獲取資料,更新dom,繫結事件監聽等,render只負責渲染,一般會等到dom載入好之後再去呼叫這些副作用方法。
useEffect(didUpdate/didMount);
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
複製程式碼
useEffect可以接受兩個引數
-
第一個引數為一個effect函式,effect函式在每次元件render之後被呼叫,相當於componentDidUpdate和componentDidMount兩個生命週期之和。effect函式可以返回一個clear effect函式,會在下一次的effect函式執行之前執行,原來componentWillUnmount裡執行的東西都可以交給它。呼叫順序是:render(dom載入完成) => prevClearUseEffect => useEffect
-
第二個引數是一個陣列,只有當陣列傳入的值發生變化時,effect才會執行。
上面的寫法如果用class實現的話應該是下面這樣的。我們按時間先後將一個會產生副作用的函式的第1次呼叫、第2-n次呼叫、解除安裝分成3截,實際上它們總是一一對應出現的,應該是一個整體。
componentDidMount() {
this.subscription = props.source.subscribe();
}
componentDidUpdate() {
this.subscription = props.source.subscribe();
}
componentWillUnmount () {
subscription.unsubscribe();
}
複製程式碼
具體案例可以看一個輪播元件的demo
import React, { useState, useEffect } from 'react';
import './index.css';
const IMG_NUM = 3;
export default () => {
const [index, setIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
// 每次元件重新整理時觸發effect, 相當cDM cDU
if (isPlaying) {
const timeout = setTimeout(() => {
// 改變state, 重新整理元件
handleNext();
}, 2000);
// 返回清除effect的回撥函式, 在每次effect呼叫完之後,如果有則執行
return () => clearTimeout(timeout);
}
// 如果不想每次render之後都調一次effect, 可以使用第二個引數作為篩選條件
}, [index, isPlaying]);
const handleNext = () => {
setIndex((index + 1) % IMG_NUM);
}
const handlePrev = () => {
setIndex((index - 1 + IMG_NUM) % IMG_NUM);
}
const handlePause = () => {
setIsPlaying(!isPlaying);
};
return (
<div>
<div className="img">{index}</div>
<button onClick={handlePrev}>prev</button>
<button onClick={handlePause}>pause</button>
<button onClick={handleNext}>next</button>
</div>
)
}
複製程式碼
useContext
const context = useContext(Context);
如果對react比較熟悉的話,應該用過Context這個api,用於在元件之間傳遞資料。useContext接受一個context物件(React.createContext生成),返回context.Consumer中獲得的值。
export const Context = React.createContext(null);
function Parent() {
const someValue = 'haha';
return (
<Context.Provider value={someValue}>
<DeepTree>
<DeepChild />
</DeepTree>
</Context.Provider>
);
}
複製程式碼
function DeepChild() {
const someValue = useContext(Context);
return (<div>{someValue}</div>)
}
複製程式碼
16.7之前的Consumer寫法是render props
function DeepChild() {
return (
<Context.Consumer>
{
(someValue) => <div>{someValue}</div>
}
</Context.Consumer>
)
}
複製程式碼
似乎還能忍受,但是但是,為了避免不必要的重新整理一般推薦用多個Context來傳遞重新整理週期不同的資料,因此按原來的render-props寫法很容易陷入多重巢狀地獄(wrapper-hell),很有可能你真正的渲染程式碼在十幾個縮排後面才開始出現。繼程式碼上下滾問題之後我們又出現了程式碼左右滾問題。
<Consumer1>
{
(value1) => (
<Consumer2>
{
(value2) => (
...
)
}
</Consumer2>
)
}
</Consumer1>
// 我怎麼還沒有被同事打死?
複製程式碼
useReducer
還有一堆高階hook
其中有一個useReducer
就是大家熟悉的那個redux裡的reducer,來段模板程式碼讓大家回憶一下。
const mapStateToProps = createStructuredSelector({
...
});
const mapDispatchToProps = (dispatch) => ({
...
});
const withReducer = injectReducer({ ... });
const withConnect = connect(mapStateToProps, mapDispatchToProps);
export default compose(withReducer, withConnect)(Component);
複製程式碼
以上的這些,使用了useReducer之後都沒有了。
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, {count: initialCount});
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'reset'})}>
Reset
</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
複製程式碼
我還用useReducer實現了一個todo的demo,程式碼分了好幾個檔案就不放上來了 github.com/lllbahol/re…
為什麼要用hook
除了上面提到的,還有官方羅列出來的一些時常會在寫class時遇到的麻煩
- class元件間不能複用與state關聯的程式碼,hook可以做到這一點。
- 複雜而龐大的class元件很難被理解,hook能夠讓你把元件拆成更小的獨立單元
- 理解class是一件困難的事,無論是對人還是對開發工具而言都是這樣。比如class裡面的this指向的是元件,在箭頭函式寫法出來之前,我們不得不手動繫結this到呼叫函式的物件上。
總的來說
用react也好久了,工程越寫越複雜,元件間的資料傳遞是一個很大的問題,從傳統的傳回撥函式,到跨多層多元件共享資料的時候使用redux,後來嫌模板程式碼太多又自己封了一層render-props結果掉進wrapper巢狀地獄的坑裡,Context出來的時候開心了一會兒然後發現依然在坑裡。寫是能寫的,就是恐懼,每寫一層,我的程式碼就又縮排了三個tab,離被同事打死又前進三步。
useContext,useReducer的用法讓我想到了高階,不同的是可以直接用變數接住而不是掛在props上,因此不用考慮props名衝突問題,但能達到高階一層層包裹資料的效果。
從現有的文件來看,新的api非常的多,一些是我們熟悉的用法一些則是完全新的東西,且暫時還沒能覆蓋所有生命週期場景(比如getDeriveStateFromProps),但不著急,可以一步一步來。
hook正式版釋出之後我還會來更新一次這個文件,在工程里正式使用一段時間之後會再更新一次,先奶一口。
參考
- www.youtube.com/watch?v=dpw… 官方介紹hook的視訊
- reactjs.org/docs/hooks-… 官方文件
- reactjs.org/docs/hooks-… 一些常見問題的官方解答