iT邦幫忙

2021 iThome 鐵人賽

DAY 14
0
Modern Web

用React刻自己的投資Dashboard系列 第 14

用React刻自己的投資Dashboard Day14 - 解決重複發送API請求的問題

tags: 2021鐵人賽 React

之前剛開始設計call api取得資料的時間點是在Card元件載入的時候才讀取,但是加上分頁功能之後,會發現一個問題,假設我從第1頁開始觀看網站,跳到第2頁看完其他圖表之後,再跳回第1頁時,會重新呼叫API來取得資料,因此在不同分頁來來回回,就會不斷呼叫一樣的API位置,但是資料都沒有改變,造成資源上的浪費。

因為我平常看的投資數據都是收盤後的資料或是總體經濟層面的數據,這些數據通常不會在幾秒或是幾分鐘之內改變,因此其實不太需要短時間內一直呼叫API,所以解決方式應該是將過去一段時間內呼叫過API的圖表數據及狀態儲存起來,若在這個時間之內又讀取同個圖表,就可以把之前呼叫過的數據拿出來使用,不需要呼叫API。

API的資料可以暫時存在什麼地方

在網路上找了關於React如何儲存資料的部份,發現一個叫做useRef的hook,可以用來做這個功能,接下來就來了解useRef是什麼,為什麼可以用來做前端暫存資料。

useRef Hook

一樣來看一下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 response

因為這次使用的投資數據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位址,所以比較節省資源一些。


上一篇
用React刻自己的投資Dashboard Day13 - 製作分頁(Pagination)功能
下一篇
用React刻自己的投資Dashboard Day15 - 投資Dashboard 2.0版 Wireframe
系列文
用React刻自己的投資Dashboard30

尚未有邦友留言

立即登入留言