iT邦幫忙

2021 iThome 鐵人賽

DAY 3
0
Modern Web

React.js 職場實戰!最常見的需求與解法!系列 第 3

React.js 職場實戰!圖片 Infinite List

一天的開始

還記得嗎?你是負責 Imager 的前端工程師,上次做了 Lazy Loading 改善了資源浪費的問題,公司對你的表現非常的滿意。

但有幹勁的你決定想想看,有沒有其他方案也可以達到節約資源的效果,可以的話甚至可以和現在的 Lazy Loading 做結合!

喝杯咖啡,逛逛文章

一邊品嚐著香醇濃郁的咖啡,一邊滑著公司配給你的 MacBook Pro,一番尋尋覓覓之後,你找到了一個很酷的技術方案——

Infinite List 無限加載列表

你稍微把文章掃過一遍,歸納出幾個實作重點:

  • 使用 Intersection Observer API
  • 一個觸發拿取新資料的 Element
  • 將原本一次拿 100 張圖(全部)改成分次拿取

上工了!開始 Coding

身為一名工程師,最快樂的時光就是動手寫程式的時候啦。如果你喜歡直接看 Code 來理解的話可以直接前往完成的範例觀看:

Edit 04-infinite-list

考量到 Lazy Loading 與 Infinite List 的結合較為複雜,這次會先實作只有 Infinite List 的版本,下一篇再將兩者結合!

<InfiniteListTrigger />

首先我們需要一個 Element 搭配 Intersection Observer 來觸發 Loading 的行為,也就是當這個 Element 出現在畫面上時,就觸發下載新的圖片資料。

我們就將這個 Element 命名為 <InfiniteListTrigger /> ,接著確保 Loading 狀態時不會顯示它,目的是避免重複觸發下載圖片列表。

// App.js
import React, { useRef } from "react";
...
import {
  ...
  Loading,
  InfiniteListTrigger
} from "./style";

const App = () => {
  const triggerRef = useRef(null);
  ...

  return (
    <View>
      ...

      {/* 並且為了防止重複觸發圖片下載,在 Loading 時我們就不顯示觸發用的 Element */}
      {!isLoading && <InfiniteListTrigger ref={triggerRef} />}
    </View>
  );
};

export default App;

fetchNewImages()

接著我們來寫一個模擬打 API 的 function,他做的事情很單純,就是等一秒之後拿到新的圖片列表。

// App.js
...

const App = () => {
  ...
  
  const fetchNewImages = () =>
    // 回傳一個 Promise 是為了模擬打 API 時非同步的情境
    new Promise((resolve) => {
      const newImages = generateImages({ count: 10 });

      // 我們假定每次打 API 平均一秒後拿到新的圖片列表
      setTimeout(() => {
        setImages((prev) => [...prev, ...newImages]);
        resolve();
      }, 1000);
    });
  
  ...
};
  
...

useInifiteList()

現在有了元件之後,我們需要一個 hook 將 <InifiniteListTrigger /> 交給 Intersection Observer 監聽。

這次要做的是相對簡單,我們需要 ref 和 onView 來完成無限加載的行為,我們看看它們對應的工作:

  • ref — 要監聽的元件,也就是 <InfiniteListTrigger />
  • onView — 當元件出現在畫面上時,觸發的 function。
// useInfiniteList.js
import { useState, useEffect } from "react";

const useInfiniteList = ({ ref, onView }) => {
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    // 要監聽的元件
    const trigger = ref.current;

    if (!trigger) return;
    if (!onView) return;

    // 這次我們的監聽行為非常的簡單
    // 如果元件出現在畫面上就觸發 onView 事件
    const callback = (entries) => {
      entries.forEach(async (entry) => {
        if (entry.isIntersecting) {
          setIsLoading(true);
          await onView();
          setIsLoading(false);
        }
      });
    };

    // 開始監聽元件
    const observer = new IntersectionObserver(callback);
    observer.observe(trigger);

    // 別忘了取消監聽元件哦!
    return () => {
      observer.unobserve(trigger);
    };
  }, [ref, isLoading, onView]);

  return { isLoading };
};

export default useInfiniteList;

準備完工

現在我們將上面的功能都組合在一起!

// App.js
import React, { useRef, useState } from "react";
import Image from "./components/Image";
import useCount from "./hooks/useCount";
import useInfiniteList from "./hooks/useInfiniteList";
import generateImages from "./utils/generateImages";
import {
  View,
  Title,
  Loading,
  ImageBlock,
  ImageCount,
  InfiniteListTrigger
} from "./style";

const App = () => {
  const triggerRef = useRef(null);
  const [images, setImages] = useState([]);
  const { count, addCount } = useCount();

  // 這裡我們製作了一個下載圖片列表的 function
  // 當使用者看到 <InfiniteListTrigger /> 的時候就觸發下載圖片列表
  const fetchNewImages = () =>
    // 回傳一個 Promise 是為了模擬打 API 時非同步的情境
    new Promise((resolve) => {
      const newImages = generateImages({ count: 10 });

      // 我們假定每次打 API 平均一秒後拿到新的圖片列表
      setTimeout(() => {
        setImages((prev) => [...prev, ...newImages]);
        resolve();
      }, 1000);
    });

  // ref   : 監聽的元件
  // onView: 當元件出現在畫面上時,執行此事件
  const { isLoading } = useInfiniteList({
    ref: triggerRef,
    onView: fetchNewImages
  });

  return (
    <View>
      <ImageCount>圖片載入數量:{count}</ImageCount>

      <Title>Imager</Title>

      <ImageBlock>
        {images.map((image) => (
          <Image key={image.id} src={image.src} onLoad={addCount} />
        ))}
      </ImageBlock>

      {/* 當我們在下載新的圖片列表時,顯示 Loading 讓使用者知道圖片正在下載 */}
      {isLoading && <Loading>Loading...</Loading>}

      {/* 並且為了防止重複觸發圖片下載,在 Loading 時我們就不顯示觸發用的 Element */}
      {!isLoading && <InfiniteListTrigger ref={triggerRef} />}
    </View>
  );
};

export default App;

成果發表

你埋首在程式之中也經過了好一段時間,現在可以來看看最終成果了!

Edit 04-infinite-list

看起來運作的很順利,當頁面滑到最下方之後,我們才開始下載下面 10 筆的資料。看到這樣的成果,你不禁露出了驕傲的神情。

咕嚕咕嚕…

快樂的時光總是過得特別快,從肚子傳出的咕嚕聲將你的注意力拉回現實,居然已經是下班時間了!你緩緩的從座位站起身子,拿著錢包準備去吃晚餐啦!


上一篇
React.js 職場實戰!圖片 Lazy Loading
系列文
React.js 職場實戰!最常見的需求與解法!3

尚未有邦友留言

立即登入留言