2021鐵人賽
React
在Day9說明了useEffect的用法,不過其實當Card.js渲染時,會發現console有錯誤訊息如下:
src\components\Charts\Card.js
Line 47:6: React Hook useEffect has missing dependencies: 'fetchData' and 'props.item.series_id'. Either include them or remove the dependency array react-hooks/exhaustive-deps
這個錯誤訊息是ESLint提醒少放了dependencies(以下簡稱deps),並建議將fetchData與props.item.series_id放入deps,而ESLint建議將這兩個物件放入deps的原因如下:
根據ESLint的建議,就把上述兩個物件放入deps,程式碼改成下面這樣:
其他程式碼不變,只改了useEffect內的deps。
const Card = (props) => {
const [chartOption, setChartOption] = useState({
...
});
const fetchData = (series_id) => {
...
};
useEffect(() => {
fetchData(props.item.series_id);
}, [fetchData, props]);
return (
...
)
}
改完之後儲存,會再跳另外一個error code
src\components\Charts\Card.js
Line 19:9: The 'fetchData' function makes the dependencies of useEffect Hook (at line 50) change on every render. Move it inside the useEffect callback. Alternatively, wrap the definition of 'fetchData' in its own useCallback() Hook react-hooks/exhaustive-deps
這個錯誤的原因是,每次render都會產生一個在記憶體上位址不同的fetchData物件(在JavaScript內,函數也是物件的一種),當JavaScript使用===(嚴格相等)判斷前後物件是否相同時,因為前後產生的fetchData位址不同,JavaScript就會回傳false,導致effect又啟動,又再次render,就又產生新的fetchData物件,造成無窮迴圈。
還好ESLint有告訴我們解法,就是wrap the definition of 'fetchData' in its own useCallback() Hook react-hooks/exhaustive-deps這句,白話一點講就是把這個function丟到useCallback裡面。
React官方範例
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
從上面的範例可以發現,用法跟useEffect很像,第一個參數是一個函式,第二個參數是deps,useCallback會回傳一個函式,如果deps沒有改變,就不會回傳新的函式,也就是說,React把這個函式記住了。
再次回到Card.js,並且用useCallback去記住fetchData這個函式,下面貼上完整程式碼:
Card.js
// import 加上 useCallback hook
import React, { useEffect, useState, useCallback } from 'react';
import styles from './Card.module.css';
import Highcharts from 'highcharts/highstock';
import HighchartsReact from 'highcharts-react-official';
const Card = (props) => {
// 初始的state加上series的name,之後setState的時候可以直接帶入,不用再用props
const [chartOption, setChartOption] = useState({
title: {
text: props.item.title
},
xAxis: {
type: "datetime",
title: {
text: 'Date'
}
},
series: [
{
name: props.item.title
}
]
});
// 將fetchData用useCallback包起來
const fetchData = useCallback((series_id) => {
fetch(`${process.env.REACT_APP_PROXY_SERVER_URL}/series/observations?series_id=${series_id}&api_key=${process.env.REACT_APP_API_KEY}&file_type=json`, {
headers: {
'Target-URL': 'https://api.stlouisfed.org/fred'
}
})
.then((response) => response.json())
.then((data) => {
let data1 = [];
data.observations.forEach(ob => {
data1.push([new Date(ob.date).getTime(), Number(ob.value)]);
});
setChartOption((prevOption) => {
return {
...prevOption,
series: [
{
name: prevOption.series[0].name,
data: data1
}
]
}
});
});
}, []);
// 在deps內加入fetchData, props
useEffect(() => {
fetchData(props.item.series_id);
}, [fetchData, props]);
return (
<div className={styles.chartFrame}>
<HighchartsReact
highcharts={Highcharts}
constructorType={'stockChart'}
options={chartOption}
/>
<div className={styles.chartInfo}>
<p className={styles.source}>source: {props.item.source}</p>
<p className={styles.date}>updated: {props.item.updated}</p>
</div>
<div>
<p className={styles.document}>{props.item.document}</p>
</div>
</div>
)
}
export default Card;
回到console應該會發現error code消失了,代表React已經成功地記住fetchData這個函式。
剛開始接觸useEffect與useCallback的時候,會覺得非常困惑,我自己是透過多次寫程式並且用React的開發者工具去修改props或是state的內容,看看React會怎麼反應,來增加我對於這個技術的認識,覺得還蠻有用的。
下一篇要再讓Card.js這個檔案的程式碼更精簡一點,會把fetchData的功能從Card.js抽出來,這樣功能會分離得更清楚一些。