2021鐵人賽
React
之前剛開始設計call api取得資料的時間點是在Card元件載入的時候才讀取,但是加上分頁功能之後,會發現一個問題,假設我從第1頁開始觀看網站,跳到第2頁看完其他圖表之後,再跳回第1頁時,會重新呼叫API來取得資料,因此在不同分頁來來回回,就會不斷呼叫一樣的API位置,但是資料都沒有改變,造成資源上的浪費。
因為我平常看的投資數據都是收盤後的資料或是總體經濟層面的數據,這些數據通常不會在幾秒或是幾分鐘之內改變,因此其實不太需要短時間內一直呼叫API,所以解決方式應該是將過去一段時間內呼叫過API的圖表數據及狀態儲存起來,若在這個時間之內又讀取同個圖表,就可以把之前呼叫過的數據拿出來使用,不需要呼叫API。
在網路上找了關於React如何儲存資料的部份,發現一個叫做useRef的hook,可以用來做這個功能,接下來就來了解useRef是什麼,為什麼可以用來做前端暫存資料。
一樣來看一下React官方的文件:
const refContainer = useRef(initialValue);
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
useRef會回傳一個mutable的物件,並且會擁有current屬性,並且這個物件會一直存在,也就是它的位置不會因為React component重新渲染而改變,但是current屬性值是可以改變的。
useRef常常會拿來取得DOM節點的內容,如下列這個Component:
function TextInputButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// 印出input當前的輸入值
console.log(inputEl.current.value)
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Print the input</button>
</>
);
}
當使用者點擊Print the input這個按鈕的時候,console會印出當前input內的值,useRef跟用useState去監聽這個input的狀態的差別在於,useState會在input有改變的時候馬上重新渲染元件,但是useRef不會。例如登入頁面的form,只要在submit的時候去取得使用者輸入的值就好,不需要在使用者仍然在輸入的期間一直去更新這個值,這樣的情況就比較適合用useRef。
useRef的特殊功能
前面有提到useRef回傳的物件會一直存在,所以當我們想要存一些變數,又不想在改變這些變數的時候重新渲染元件,useRef就很適合來做這件事。
因為這次使用的投資數據api,通常在一天之內都不會有變動,所以使用者在同一天之內如果有看過某一張圖表,也就是呼叫過某支api取得過數據,那麼當天他又要看同一張圖表的時候,應該只要把之前呼叫過的數據拿出來就好,不需要再呼叫api。
因此,需要有一個地方可以儲存API response,而且這個地方是可以讓Pagination這個元件取得資料,所以我選擇在它上一層的Charts元件放這個資料,並且用useRef來放,這樣就可以用props傳遞資料給Pagination及Card,Card也可以透過呼叫Charts內的function來儲存API response到useRef的current。
API新增回傳日期
src\components\Charts\fredAPI.js
const useFredAPI = (series_id) => {
return fetch(...)
.then((response) => response.json())
.then((data) => {
let fetchDate = data.realtime_end
let seriesData = [];
data.observations.forEach(ob => {
seriesData.push([new Date(ob.date).getTime(), Number(ob.value)]);
});
// 加上呼叫API的日期
return [seriesData, fetchDate];
})
};
export default useFredAPI;
新增useRef至Charts元件
src\components\Charts\Charts.js
// import useRef hook
import React, { useRef } from 'react';
import Card from './Card';
import Pagination from '../../UI/Pagination';
const Charts = (props) => {
// 定義儲存資料的object
const cachedData = useRef({});
// 定義修改資料的function
const saveCachedDataHandler = (seriesId, seriesData, fetchDate) => {
cachedData.current[seriesId] = {
"seriesData": seriesData,
"fetchDate": fetchDate
}
}
// props新增cachedData及onSaveCachedData
return (
<Pagination
data={props.charts}
RenderComponent={Card}
pageLimit={Math.ceil(props.charts.length / 3)}
dataLimit={3}
cachedData={cachedData}
onSaveCachedData={saveCachedDataHandler}
/>
)
};
分頁元件新增props
src\UI\Pagination.js
import React, { useState } from 'react';
import { Row } from 'react-bootstrap';
import styles from './Pagination.module.css';
const Pagination = (props) => {
const { data, RenderComponent, pageLimit, dataLimit, cachedData, onSaveCachedData } = props;
...
// 透過props再傳遞一次資料與函數
return (
<div>
<div className={styles.dataContainer}>
<Row>
{getPaginatedData().map((d, idx) => (
<RenderComponent key={idx} data={d} cachedData={cachedData} onSaveCachedData={onSaveCachedData} />
))}
</Row>
</div>
...
</div>
);
};
export default Pagination;
Card元件修改資料取得邏輯
src\components\Charts\Card.js
import ...
const Card = (props) => {
const [chartOption, setChartOption] = useState({...});
const fetchData = useCallback(async (series_id) => {
// 先從useRef找資料,沒有的話就call api再將資料儲存至useRef
try {
let seriesData, fetchDate
// get data from cachedData
if (props.cachedData.current[series_id]) {
if (Date.now() - (new Date(props.cachedData.current[series_id]["fetchDate"])).getTime() < 57600000) {
seriesData = props.cachedData.current[series_id]["seriesData"]
}
} else {
// get data from api
[seriesData, fetchDate] = await fredAPI(series_id);
props.onSaveCachedData(series_id, seriesData, fetchDate)
}
// setState
setChartOption((prevOption) => {
return {
...prevOption,
series: [
{
name: prevOption.series[0].name,
data: seriesData
}
]
}
})
} catch (error) {
console.log(error)
}
}, [props]);
useEffect(...);
return (...)
}
export default Card;
寫好之後可以發現,打開DevTool的Network,發現只有剛開始call了一次API,後來在分頁之間來回跳轉時,就不會再call同樣的api位址,所以比較節省資源一些。