用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位址,所以比较节省资源一些。


<<:  第一次当社群讲者

>>:  [Day29] CH13:画出你的藏宝图——事件处理(下)

【Day 23】Class 类别(续)

前言 在学习程序语言的过程中,应该都有听过物件导向程序设计(Object-oriented prog...

Day-21 RadioGroup

使用者可在一个RadioGroup底下,建立多个RadioButton。 而RadioGroup与C...

DAY 30『 从相簿选取照片( 有裁剪照片功能 ) 』ImagePicker - Part2

在 @IBAction 里 令 vc 为 UIImagePickerController let v...

Day 20 - [语料库模型] 08-绘制语料库模型Heatmap图

莫烦 Python 的原版程序码: https://github.com/MorvanZhou/NL...

Day 27 - Rancher Fleet Kustomize 应用程序部署

本文将於赛後同步刊登於笔者部落格 有兴趣学习更多 Kubernetes/DevOps/Linux 相...