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