Infinite Scroll with data-query

Append the next page to the query's rows; row identity keeps the list stable while it grows.

The plain Infinite Scroll pattern pushes fetched items onto an array in component state and guards against double loads by hand. With a data-query (Full build), the next page is refresh({ append: true }): the query accumulates the rows, dedupes them by key, and marks itself isStale while the page is in flight.

Live Demo

The demo runs in its own frame on the Full build. Open it on its own ↗

Source

HTML + JavaScript
<div data-component="feed-view">
    <div class="feed" data-action="scroll:checkScroll">
        <div data-list="items" data-key="id">
            <template><div class="item" data-bind="title"></div></template>
        </div>
    </div>
    <span data-bind="$feed.count"></span> loaded
    <span data-show="$feed.isStale">Loading more…</span>
    <button data-action="loadMore" data-bind-attr="{ disabled: !hasMore }">Load more</button>
</div>

<script>
// The page number lives in a store so every request the query makes
// reads the same value: the source reads it, the component advances it.
wildflower.store('pager', { state: { page: 1 } });

wildflower.query('feed', {
    from: '/api/feed',
    key: 'id',
    params: () => ({ page: wildflower.getStore('pager').page })
});

wildflower.component('feed-view', {
    state: { hasMore: true, busy: false },
    computed: {
        items() { return wildflower.getQuery('feed').rows; }
    },
    checkScroll(event, element) {
        if (element.scrollTop + element.clientHeight >= element.scrollHeight - 24) this.loadMore();
    },
    async loadMore() {
        if (this.busy || !this.hasMore) return;
        this.busy = true;
        wildflower.getStore('pager').page++;
        await wildflower.getQuery('feed').refresh({ append: true });   // accumulate, don't swap
        this.hasMore = wildflower.getQuery('feed').count < 48;
        this.busy = false;
    }
});
</script>

Key Points

  • refresh({ append: true }) adds the new page to the rows already on screen instead of replacing them; append needs a key, and a row that arrives twice updates in place rather than duplicating
  • The page number belongs in a store the source reads (here through params), so a later focus refresh or poll asks for the page the user is looking at rather than snapping back to page one
  • After the first append, background refreshes merge: new rows land at the head, existing rows update in place, and the user keeps their place; a bare refresh() is the reset that starts over
  • isStale is the "loading more" indicator and count the loaded total; hasMore and the scroll sentinel stay in your component, since the query ships the data primitive and not the widget
  • Needs the Full build; the plain pattern keeps the array in component state. Numbered pages, deletes in a windowed fetch and ordering are in Paged and Infinite Results