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(() => {
|
$effect(() => {
|
||||||
if (!sentinel || !bookCollection.moreBooks) return;
|
if (!sentinel || !bookCollection.moreBooks) return;
|
||||||
|
const target = sentinel;
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
async (entries) => {
|
async (entries) => {
|
||||||
if (entries[0].isIntersecting && !bookCollection.loading) {
|
if (!entries[0].isIntersecting) return;
|
||||||
await bookCollection.loadMoreBooks();
|
|
||||||
|
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,
|
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 () => {
|
return () => {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
@@ -110,7 +122,7 @@
|
|||||||
|
|
||||||
{#if bookCollection.moreBooks}
|
{#if bookCollection.moreBooks}
|
||||||
<div bind:this={sentinel}>
|
<div bind:this={sentinel}>
|
||||||
{#if bookCollection.loading}
|
{#if bookCollection.loadingMore}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export class BookCollectionState {
|
|||||||
public moreBooks = $state(false);
|
public moreBooks = $state(false);
|
||||||
private currentBookPage = $state(1);
|
private currentBookPage = $state(1);
|
||||||
public loading = $state(false);
|
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
|
* Whether the reader has loaded past the first page. Anything that refetches
|
||||||
@@ -165,22 +167,37 @@ export class BookCollectionState {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.books = [...result.items];
|
this.books = [...result.items];
|
||||||
|
this.moreBooks = result.total > result.items.length;
|
||||||
this.currentBookPage = 1;
|
this.currentBookPage = 1;
|
||||||
this.loading = false;
|
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() {
|
async loadMoreBooks() {
|
||||||
const result = await this.ops.listBooks({
|
if (this.loadingMore || !this.moreBooks) return;
|
||||||
currentPage: this.currentBookPage + 1,
|
|
||||||
pageSize: 50,
|
|
||||||
sortOrder: this.sortOrder,
|
|
||||||
orderBy: this.orderBy,
|
|
||||||
...this.filters
|
|
||||||
});
|
|
||||||
|
|
||||||
this.books = [...this.books, ...result.items];
|
this.loadingMore = true;
|
||||||
this.moreBooks = result.total > this.books.length;
|
try {
|
||||||
this.currentBookPage++;
|
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>) {
|
updateBooks(books: PaginatedResponse<Book>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user