Infinite Scroll
Problem
A social feed or activity log should append new items as the user scrolls, rather than replacing the current page like traditional pagination. You need to track how many pages have been loaded, expose a "load more" affordance, and reset the accumulated list when the user searches or filters.
Solution
Use createInfiniteSource(). It accumulates every fetched page into source.current; call loadMore() to append the next page.
import { createInfiniteSource } from '@vielzeug/sourcerer';
type Post = { id: number; title: string };
const source = createInfiniteSource<Post>({
fetch: async ({ limit, page, search }, signal) => {
const params = new URLSearchParams({ limit: String(limit), page: String(page) });
if (search) params.set('q', search);
const res = await fetch(`/api/posts?${params}`, { signal });
return res.json(); // { items: Post[], total: number }
},
limit: 10,
});
await source.ready();
console.log(source.current.length, source.meta.hasMore, source.meta.loadedPages);Wiring to a scroll sentinel
const sentinel = document.querySelector('#load-more-sentinel')!;
const observer = new IntersectionObserver((entries) => {
if (entries[0]?.isIntersecting && source.meta.hasMore && !source.meta.isLoadingMore) {
void source.loadMore();
}
});
observer.observe(sentinel);
// Re-render whenever more items arrive
source.subscribe(() => renderFeed(source.current, source.meta));loadMore() is already a no-op when meta.hasMore is false or a fetch is already in flight, but guarding in the observer callback avoids scheduling redundant microtasks on every intersection event.
Searching restarts the feed
await source.search('release notes', { immediate: true });
// current is cleared immediately, loadedPages resets to 0, then page 1 of the filtered results loadsBoth search() and reset() clear the accumulated current array right away (not just after the fetch settles) — the feed visibly empties before repopulating, rather than showing a stale, soon-to-be-replaced list during the fetch.
Pitfalls
meta.isLoadingMoreistrueonly duringloadMore()fetches.meta.isLoadingcovers the initial fetch andreset()/search()refetches — check the one that matches the UI state you're rendering (e.g. a full-page spinner vs. a small spinner at the bottom of the list).- There is no
refresh()onInfiniteSource(unlikeRemoteSource/CursorSource) — to re-fetch from scratch, callreset(), which also clearsloadedPagesand accumulated items. patch()only acceptslimit/search, and always resets to page 1 with an empty accumulator — it is not a way to "load more with a different limit" for already-loaded pages.