iT邦幫忙

2021 iThome 鐵人賽

DAY 17
1

元件介紹

Infinite scroll 能在面對多筆資料時,讓捲軸滑動到底部時再載入下一頁面的資料。

由於一次性向後端取得大批的資料,對於後端的資料計算、資料透過網路傳輸、頁面的渲染,在效能上都有可能會有影響,因此將資料分批載入也有助於網頁效能的優化。

另一個分批載入常見的做法是使用 Pagination,分頁載入,雖然都是分批載入,但是使用情境有一些區別, Infinite scroll 的優點是一直往下滑就會自動有資料載入,操作效率較流暢,但缺點是難以回去找剛剛看過的東西。所以如果網頁內容是希望讓使用者能夠有效率地找尋特定資訊時,這時選擇 Pagination 會比 Infinite scroll 較為適合。

參考設計 & 屬性分析

Infinite scroll 的特點是讓資料滾到底部時自動載入,所以這邊的關鍵是,我們要如何判斷「是否已經滾動到底部」?

從上圖可以知道,Element.scrollHeight 表示元件的可滾動範圍;Element.scrollTop 指的是元素被向上滾動的高度,換句話說就是你已經走過的距離;最後 Element.clientHeight 就是指元素內部高度,也就是滾動可視範圍的高度。

Element.scrollTop + Element.clientHeight >= Element.scrollHeight

所以「滾動到底部」換句話來說,就是你滾過的距離加上自己元素的高度,大於等於可滾動範圍的高度。

介面設計

屬性 說明 類型 默認值
height 元件高度 number
isLoading 載入中狀態 boolean false
onScrollBottom 滑動到底部的 callback function
children 內容 list of ReactNode

元件實作

以下是我們想像當中的 InfiniteScroll,在元件的 children 當中就是被瀏覽的內容,所以被 InfiniteScroll 包起來的內容我們希望被不斷的載入。

因為他是一個可被滑動的範圍,所以這個容器需要被限制高度,內容超出這個高度才有辦法被 scroll。

再來我們需要在滑動到底部的時候觸發事件,例如需要去打某支 API 來載入資料,因此我們提供一個 onScrollBottom 的 callback。

然後我們在打 API 的時候,是一個非同步行為,會有載入中的狀態,因此也有一個 isLoading 的 Boolean props:

<InfiniteScroll
  height={250}
  isLoading={isLoading}
  onScrollBottom={() => {}}
>
  {...}
</InfiniteScroll>

如下程式碼所示,我們需要透過 useRef 讓我們能夠操作這個容器的 DOM,因為我們需要計算何時滑動到底部:

const infiniteScrollRef = useRef();

<InfiniteScrollWrapper
  ref={infiniteScrollRef}
  $height={height}
  onScroll={handleOnScroll}
>
  {children}
  {isLoading && <Loading />}
</InfiniteScrollWrapper>

前面分析我們也已經介紹過如何判斷滑動到底部的方法,因此在 onScroll 的時候,我們需要去觸發這個計算:

const handleOnScroll = () => {
  const containerElem = infiniteScrollRef.current;
  if (containerElem) {
    const scrollPos = containerElem.scrollTop + containerElem.clientHeight;
    const divHeight = containerElem.scrollHeight;
    
    // 滾過的距離加上自己元素的高度,大於等於可滾動範圍的高度
    if ((scrollPos >= divHeight) && onScrollBottom) {
      onScrollBottom();
    }
  }
};

這樣我們簡單的 InfiniteScroll 就搞定了!
我們展示一下成果:

從上圖來看,當我們滑動到底部的時候,就會去觸發 GET api 來取得下一頁的資料,並且將資料更新到畫面上。

我使用的方式是,在 onScrollBottom 被呼叫的時候,表示他滑到底部了,所以我要取得下一頁的資料,因此我透過 setPage 這個 useState 將 page + 1

const [page, setPage] = useState(1);

<InfiniteScroll
  height={250}
  isLoading={isLoading}
  onScrollBottom={() => {
    if (!isLoading) {
      setPage((prev) => prev + 1);
    }
  }}
>
  {
    dataSource.map(({ id, author, download_url }) => (
      <ListItem
        key={id}
        author={author}
        url={download_url}
      />
    ))
  }
</InfiniteScroll>

當 page 這個 state 被改變的時候,我就要去打 API 來載入資料,所以我這便是透過 useEffect 來實作,並且他的 comparison array 裏面就放了 page,表示 page 被改變的時候,需要執行裡面的內容:

useEffect(() => {
  setSideEffect({
    ...defaultSideEffect,
    isLoading: true,
  });

  fetch(`https://picsum.photos/v2/list?page=${page}&limit=${limit}`, {})
  .then((response) => {
     setSideEffect({
       ...defaultSideEffect,
       isLoaded: true,
     });
     return response.json();
		})
    .then((jsonData) => {
      setDataSource((prev) => [...prev, ...jsonData]);
    }).catch((error) => {
      setSideEffect({
        ...defaultSideEffect,
        error,
      });
    });
}, [page]);

InfiniteScroll 元件原始碼:
Source code

Storybook:
InfiniteScroll

參考

https://codesandbox.io/s/yk7637p62z?file=/src/index.js

https://github.com/TimingJL/github-repos-search/blob/master/src/containers/MainPage/MainContent/index.tsx


上一篇
【Day16】數據展示元件 - Table
下一篇
【Day18】導航元件 - Breadcrumb
系列文
30 天擁有一套自己手刻的 React UI 元件庫30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言