【Day17】数据展示元件 - Infinite scroll

元件介绍

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


<<:  [Day 19] 第一主餐 pt.11-财报资料,我全都要

>>:  Day 16:架设 Grafana (2)

[DAY 28] _看门狗简介_视窗看门狗(2)

昨天主要介绍了视窗看门狗和独立看门狗的差别,今天来看这如何计算,这计算方式再参考手册里面有举例说明,...

Router

路由架构 Breeze 已经架构好利用 inertia.js 取得 Login 等画面的路由,不过为...

入场後,重要性排序

投资人入场後,一定会面临到以下三种情况,依照重要性排序,我会一一说明,到达这个阶段你所要做的事。 将...

[Day5] Vite 出小蜜蜂~ Component 元件!

Day5 写程序写到一定的阶段後,会开始发现,其实做出想要的功能并不困难。 真正难的,其实是如何写出...

[Day4] 预设范例帐户:OE

这篇文将介绍资料库中的预设帐户之一OE并介绍各个表格和他们之间的关系。 纲目:所有的资料库物件。OE...