【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)

网速单位的陷阱:bps

聊了这麽多上网的服务,或许大家最在意的还是上网的速度吧! 但你知道 ISP 们平常所说的网路速度和你...

【Day 14】海绵宝宝的神奇海螺:对 AWS 发出 Support

tags: 铁人赛 AWS business support codepipeline codebu...

Day 23 : 集成式学习

接下来的要介绍一个很重要的模型观念 - 集成式学习。俗话说的好,三个臭皮匠胜过一个诸葛亮,而在机器学...

Day 24 Ruby 哪些不是物件

在 Ruby 内几乎所有的东西都是物件。 在说明答案前先来看看甚麽是 OOP 及物件吧。 Objec...

[Day24] Scrum失败经验谈 – 壁垒分明的职务配置

不足的丰富资源 未依团队性质配置的资源,会制造资源不足的假象 在IT团队最大的时候,有11人,分别是...