feat: fetch the next page of books before the reader hits the bottom
The prefetch zone is a viewport deep rather than 200px, and the observer re-observes after a page lands so a chain of loads is not cut short.
This commit is contained in:
@@ -29,19 +29,31 @@
|
||||
|
||||
$effect(() => {
|
||||
if (!sentinel || !bookCollection.moreBooks) return;
|
||||
const target = sentinel;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
async (entries) => {
|
||||
if (entries[0].isIntersecting && !bookCollection.loading) {
|
||||
await bookCollection.loadMoreBooks();
|
||||
if (!entries[0].isIntersecting) return;
|
||||
|
||||
await bookCollection.loadMoreBooks();
|
||||
|
||||
// A page that does not push the sentinel back out of the prefetch zone
|
||||
// produces no further intersection change, so the observer would sit
|
||||
// idle until the next scroll. Re-observing asks for a fresh reading.
|
||||
if (bookCollection.moreBooks) {
|
||||
observer.unobserve(target);
|
||||
observer.observe(target);
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
root: scrollContainer,
|
||||
rootMargin: '0px 0px 200px 0px'
|
||||
// One viewport of lead time: the next page is requested a full screen
|
||||
// before the reader reaches the end of the current one.
|
||||
rootMargin: '0px 0px 100% 0px'
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(sentinel);
|
||||
observer.observe(target);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
@@ -110,7 +122,7 @@
|
||||
|
||||
{#if bookCollection.moreBooks}
|
||||
<div bind:this={sentinel}>
|
||||
{#if bookCollection.loading}
|
||||
{#if bookCollection.loadingMore}
|
||||
<Spinner />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,8 @@ export class BookCollectionState {
|
||||
public moreBooks = $state(false);
|
||||
private currentBookPage = $state(1);
|
||||
public loading = $state(false);
|
||||
/** A page append, as opposed to `loading`, which replaces the whole list. */
|
||||
public loadingMore = $state(false);
|
||||
|
||||
/**
|
||||
* Whether the reader has loaded past the first page. Anything that refetches
|
||||
@@ -165,22 +167,37 @@ export class BookCollectionState {
|
||||
});
|
||||
|
||||
this.books = [...result.items];
|
||||
this.moreBooks = result.total > result.items.length;
|
||||
this.currentBookPage = 1;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetching means the trigger can fire again while a page is still in
|
||||
* flight, so the guard is here rather than in the observer — every caller
|
||||
* gets it, and a second call is dropped instead of duplicating a page.
|
||||
*/
|
||||
async loadMoreBooks() {
|
||||
const result = await this.ops.listBooks({
|
||||
currentPage: this.currentBookPage + 1,
|
||||
pageSize: 50,
|
||||
sortOrder: this.sortOrder,
|
||||
orderBy: this.orderBy,
|
||||
...this.filters
|
||||
});
|
||||
if (this.loadingMore || !this.moreBooks) return;
|
||||
|
||||
this.books = [...this.books, ...result.items];
|
||||
this.moreBooks = result.total > this.books.length;
|
||||
this.currentBookPage++;
|
||||
this.loadingMore = true;
|
||||
try {
|
||||
const result = await this.ops.listBooks({
|
||||
currentPage: this.currentBookPage + 1,
|
||||
pageSize: 50,
|
||||
sortOrder: this.sortOrder,
|
||||
orderBy: this.orderBy,
|
||||
...this.filters
|
||||
});
|
||||
|
||||
this.books = [...this.books, ...result.items];
|
||||
// An empty page means the count we were given was stale; stop asking
|
||||
// rather than loop on a page that never grows the list.
|
||||
this.moreBooks = result.items.length > 0 && result.total > this.books.length;
|
||||
this.currentBookPage++;
|
||||
} finally {
|
||||
this.loadingMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateBooks(books: PaginatedResponse<Book>) {
|
||||
|
||||
Reference in New Issue
Block a user