2021鐵人賽
React
既上一篇介紹完useState hook後,本篇就來介紹Day6也有用到的useEffect hook,在React官網有提到,如果使用者熟悉React class component的生命週期,那麼可以用下面這個提示來理解useEffect:
如果你熟悉 React class 的生命週期方法,你可以把 useEffect 視為 componentDidMount,componentDidUpdate 和 componentWillUnmount 的組合。
從下面這張React官方提供圖可以看出React生命週期的三個階段,Mounting(創建)、Updating(更新)、Unmounting(銷毀),不過要完整地解釋這張圖會需要比較多的篇幅,這邊只要先了解執行順序為先是componentDidMount,再來是componentDidUpdate,最後是componentWillUnmount,再來就直接看官網的範例即可。
要知道用法上的差異,可以從React官網上的範例來理解,這個範例是用React去寫一個計數器,並且在React對DOM進行變更後立即更新網頁標題,下面就分別用class與hook去寫,藉由範例認識其差異。
先將相同功能的程式碼比較一下,使用hook的程式碼比較短一些,也發現class版本需要用到componentDidMount與componentDidUpdate去實現這樣的功能,而hook版本只需要一個useEffect就可以處理,當然並不是程式碼短就是好,因此接下來分析一下功能差別。
因為我們想要在一進入網頁的時候,就讓網頁的title顯示"You clicked 0 times",因此需要在Mounting的時候呼叫一次,之後在點擊按鈕的時候,count這個state會被更新,為了要將網頁的title也更新為"You clicked n times",因此需要呼叫componentDidUpdate。
可能有人會說,難道沒有一個method可以讓React在mounting及updating都去執行這行程式碼嗎?可惜React的class component剛好就是沒有這個功能,因此就需要寫兩次。
將上圖中的class版本程式碼放大如下:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
相對於class版本的程式碼,hook版本只寫了一行useEffect就達成這個範例的要求,原因是useEffect會在每次重新render之後都執行一次,不管這個render是在mounting階段或updating階段所執行的,useEffect都會執行,因此當state變動,自然會重新render,也就啟動useEffect內的程式,網頁的title也就更新了,真的是非常方便。
然後還有一個小地方是,useEffect需要在component內呼叫,才能去抓到最新的state,這個是javascript closures的特點,而React巧妙地運用了這點。
將上圖中的hook版本程式碼放大如下:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
在Card.js中,因為我希望在以下時間點去觸發抓取數據的程式:
從上述需求可以發現,抓取數據這個動作,會在Mounting及Updating階段執行,因此useEffect就符合這樣的需求。所以在Card.js內寫了一個fetchData函數,並在useEffect內呼叫它,即完成這個功能。
Card.js
const Card = (props) => {
// 定義state
const [chartOption, setChartOption] = useState({...});
// 抓取API數據
const fetchData = (series_id) => {...};
// effect
useEffect(() => {
fetchData(props.item.series_id);
}, []);
return (
...
)
}
在上面的程式碼可以看到useEffect內有一個空的陣列,它的功能是避免程式陷入無限迴圈中,原因如下圖,在mount時執行render,完成之後effect就會啟動,因此就變更了state,React發現state改變後又重新render,再來又觸發effect,因此就形成了一個無限迴圈。
這個無限迴圈的問題,可以透過useEffect的dependencies來解決,例如官網的範例如下:
如果count沒有改變,effect就不會執行。
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
因此我在Card的useEffect放入一個空陣列當作dependencies,因為它一直都是空的,所以react只會執行一次effect,雖然這不是最正確的方式,不過算是暫時解決這個問題。
useEffect真的是有點複雜的功能,不過還蠻好用的,在這篇提到的無限迴圈問題還沒結束,下一篇要來研究到底useEffect中的dependencies到底要放一些什麼,同時也會提到useCallback這個hook。