Hook 改變的 React Component 寫法思路
React hook 在醞釀近一年後,終于在16.8穩定版中重磅推出。在此前後FB的Dan大神勞心勞力地幾乎每天在Twitter上給大家洗腦宣傳。
那它究竟跟之前的React應用相比有什麼改變?
前提是先要了解Memoize
Memoize是貫穿hook用法的一個非常重要的概念,了解它是正确使用hook的基石。
Memoize基本上就是把一些程式中一些不需要反複計算的值和上下文(context)儲存在記憶體中,起到類似緩存的作用,下次運作計算時發現已經有計算并儲存過這個值就直接從記憶體中讀取而不再重新計算。
Javascript中比較常見的做法可參考lodash.memoize源代碼,它通過給function設定一個Map的屬性,将function的傳參作為key,運作結果存為這個key的value值。下次調用這個function時,它就先去檢視key是否存在,存在的話就直接将對應的值傳回,跳過運作方法裡的代碼。
這在functional programming中非常的實用。不過也就是說,隻有所謂純粹的function才能适用這種方式,輸入和輸出是一一對應的關系。
React hooks都是被Memoize的,綁定在使用的component中,隻有指定的值發生了變化,這個hook中的代碼和代碼上下文才會被更新和觸發。讓我在下文中一步步說明。
用useState替換this.setState
在hook之前,用function寫的Component是無法擁有自己的狀态值的。想要擁有自己的狀态,隻能痛苦地将function改成Class Component。
Class Component中使用this.setState來設定一個部件的狀态:
class MyComponent extends React.Component{
constructor(props) {
super(props);
// 初始化state值
this.state= {
myState: 1
};
this.toggleState = this.toggleState.bind(this);
}
// 将myState在0和1之間切換
toggleState() {
this.setState(prevState => {
return { myState: 1 - prevState.myState };
});
}
render() {
return <button onClick={toggleState}>Toggle State</Button>;
}
}
使用React hook之後這就可以在function component中實作,并且更為簡潔:
function MyComponent (props) {
const [myState, setMyState] = useState(1);
// 這裡應該用useCallback, 會在後面說明
const toggleState = () => setMyState(1 - myState);
return <button onClick={toggleState}>Toggle State</button>;
}
代碼行數精簡為原來的三分之一之外,使用起來也更加地直覺。
useState
接收一個值作為一個state的初始值,傳回一個數組。這個數組由兩個成員組成,第一個成員是這個狀态的目前值(如上例中的
myState
),第二個成員是改變這個狀态值的方法,即一個專屬的
setState
(如上例中的
setMyState
)。
During the initial render, the returned state () is the same as the value passed as the first argument (
state
).
initialState
注意這個初始值隻在初始渲染中才被指派給對應變量,也就是隻有在component第一次挂載時,渲染之前才做了一次初始化。後來更新引發的重新渲染都不會讓初始值對狀态産生影響。
useEffect替換Component生命周期函數
useEffect并不能等同或替代原有的component生命周期函數,他們設計的思路完全不同。對于長期習慣使用原有生命周期的人來說,可能需要從“替換”的角度來轉換寫react代碼的思維方式。
簡單來說,鈎子的設計更符合“react”這個名字。它完全是通過對資料和狀态變化的檢測,來“回報”更新。
在前端程式裡,我們習慣了一種我稱為“事件思維”的方式,就是說發生某件事,就調用某段代碼。運用鈎子,就是把某些資料的變化作為事情發生的标志。
在此之前,我們回顧一下類component中幾個主要的生命周期。
剛挂載:
- constructor()
- UNSAFE_componentWillMount()
- getDerivedStateFromProps()
- render()
- componentDidMount()
屬性或狀态更新:
- getDerivedStateFromProps()
- shouldComponentUpdate()
- UNSAFE_componentWillUpdate()
- UNSAFE_componentWillReceiveProps()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
取消挂載:
- componentWillUnmount()
componentDidUpdate & shouldComponentUpdate
如上可以看到componentDidUpdate是隻在:
- 屬性值或狀态值發生變化
- 并且shouldComponentUpdate()傳回為true(預設為true)
時才會被觸發的。
useEffect傳入的函數,則會在:
- component 挂載後觸發一次
- 渲染完成後才觸發
- 第二個參數裡傳入的需要檢測比較的資料有變化時才觸發
- 如果沒有第二個參數,則每次渲染都會觸發
那就很明顯useEffect不能簡單地替代componentDidUpdate。
但在實際使用過程中,我們通常會:
- 在componentDidMount時調用API加載資料
- componentDidUpdate裡比較一些條件(比如傳入的資料id發生變化)後可能再次調用同樣API重新加載資料
- 并且用shouldComponentUpdate來避免不必要的重新渲染(比如id沒變化,redux store裡這個id對應的資料也沒變化的時候)
如下:
class MyComponent extends React.Component {
componentDidMount() {
this.loadData();
}
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.loadData();
}
}
shouldComponentUpdate(nextProps) {
return nextProps.id !== this.props.id ||
nextProps.data !== this.props.data;
}
loadData() {
this.props.requestAPI(this.props.id);
}
render() {
return <div>{this.props.data}</div>;
}
}
useEffect就合并并且大大地簡化了這一過程:
function MyComponent(props) {
React.useEffect(() => {
props.requestAPI(props.id);
}, [props.id, props.requestAPI);
return <div>{props.data}</div>;
}
export default React.memo(MyComponent);
上述例子中實際上是将componentDidMount和componentDidUpdate合并了。在shouldComponentUpdate中比較props.data是否變化這一步,借由React.memo來完成。注意它隻做每個props值的淺比較。
componentDidMount
那你說componentDidUpdate也許經常做compinentDidMount裡會做的事情,那麼componentDidMount呢?它是必須的。
雖然react大牛們已經提議了一些concurrent的方法,但react發了那麼多版依舊沒被加進來。是以在鈎子的官方文檔裡,我們找到這一段:
If you pass an empty array (), the props and state as inside the effect will always have their initial values. While passing
[]
as the second argument is closer to the familiar
[]
and
componentDidMount
mental model, there are usually bettersolutions to avoid re-running effects too often. Also, don’t forget that React defers running
componentWillUnmount
until after the browser has painted, so doing extra work is less of a problem.
useEffect
也就是說可以給useEffect第二個參數傳一個空數組,來暫代componentDidMount。雖然官方不推薦,但現在沒辦法隻能這麼幹。。。
so,上面已經說了,第二個參數是用來将某些資料變化作為效果觸發的依據。那空數組,首先就防止了第二個參數為空時每次render都會觸發的場景,然後每次渲染都沒有資料可以比較變化,那就隻有component挂載時才能被觸發了。
function MyComponent() {
React.useEffect(() => {
console.log('MyComponent is mounted!');
}, []);
return null;
}
getDerivedStateFromProps
useState與useEffect不同,它不會檢測資料的變化,它隻接收一個參數 - 它的初始值。初始化過後,所有的狀态更新,都需要我們自己調用useState所傳回的 ”setState“方法來完成。
這是因為我們已經有useEffect了。getDerivedStateFromProps的效果等同于:
function MyComponent(props) {
const [intValue, setIntValue] = React.useState(props.value);
React.useEffect(() => {
setIntValue(parseInt(props.value, 10));
}, [props.value, setIntValue]);
return <div>{intValue}</div>;
}
比起getDerivedStateFromProps,這種方式還有效防止了不必要的多次計算。
componentWillUnmount
componentWillUnmount是又一個非常重要常用的生命周期。我們通常用它來解綁一些DOM事件,清理一些會造成記憶體洩漏的東西。
這就要說到useEffect裡第一個參數的回調函數,是可以傳回一個函數用來做這種清潔工作的:
React.useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
以上是官方文檔中的一個例子,它等同于:
class MyComponent extends React.Component {
componentDidMount() {
this.props.source.subscribe();
}
componentDidUpdate(prevProps) {
if (prevProps.source !== this.props.source) {
prevProps.source.unsubscribe();
this.props.source.subscribe();
}
}
componentWillUnmount() {
this.props.source.unsubscribe();
}
render() {
// ...
}
}
簡單來說,這個傳回的清潔函數,會在下一次該效果被觸發時首先被調用。