Infinite Feed

A feed that never ends has to fetch while the viewer watches: early enough that the next post is already there, with something to show while a page is on its way or has failed, and without a request left running after the feed is gone.

Try it

Every version imports ./api, which stands in for your backend: fetchPosts(page, signal) returns one page of posts and stops when the signal aborts. Replace it with a call to your API, and drop failNextRequest, which only drives the demo's failure switch.

typescript
import { createSignal } from '@reelkit/core';
import { cdnUrl } from '@reelkit/example-data';

// Stands in for your backend. The recipe only relies on the signature of
// `fetchPosts`: a page number in, a page of posts out, cancellable through an
// AbortSignal.

export interface Post {
  id: string;
  title: string;
  image: string;
}

const kPageSize = 6;
const kDelayMs = 800;
const kImages = Array.from(
  { length: 9 },
  (_, i) => `samples/images/image-${String(i + 1).padStart(2, '0')}.jpg`,
);

/**
 * Demo switch: while on, the next request fails, so the error slide can be
 * seen. A signal, so the switch shows when a request has used it up. Only the
 * demo on this page uses it: delete it, and the `@reelkit/core` import with
 * it, when you replace `fetchPosts` with your API.
 */
export const failNextRequest = createSignal(false);

export function fetchPosts(page: number, signal: AbortSignal): Promise<Post[]> {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      if (failNextRequest.value) {
        failNextRequest.value = false;
        reject(new Error('The feed could not be loaded.'));
        return;
      }
      resolve(
        Array.from({ length: kPageSize }, (_, i) => {
          const n = page * kPageSize + i;
          return {
            id: `post-${n}`,
            title: `Post ${n + 1}`,
            image: cdnUrl(kImages[n % kImages.length]),
          };
        }),
      );
    }, kDelayMs);

    signal.addEventListener(
      'abort',
      () => {
        clearTimeout(timer);
        reject(signal.reason);
      },
      { once: true },
    );
  });
}
The sample media works only on reelkit.dev

cdnUrl builds links to cdn.reelkit.dev, which serves its files only to the ReelKit sites. In your app those images fail to load, so point image at your own media before you copy this file.

InfiniteFeed.tsx
InfiniteFeed.vue
infinite-feed.component.ts

Swipe up or press ↓: a new page arrives three posts before the end. Press "Fail next request" to see the error slide and its Retry.

Why it works

  • The count grows, the slides do not. count is the number of posts plus one, and only three slides are ever in the DOM, so a feed of thousands costs the same as a feed of ten. See virtualization.
  • The last slide is the status. The extra slide past the posts shows "Loading…" while a page is on its way and the error with Retry when it failed, so the viewer never swipes into nothing. It is one more case in the itemBuilder, which renders whatever the index stands for.
  • Fetch ahead, from the slide change. afterChange asks for the next page once the viewer is three posts from the end. A failed page waits for Retry, so swiping back and forth does not repeat a failing request. The callback is listed with the other events for React, Vue, and Angular.
  • Keys follow the posts. keyExtractor returns each post's id, so a slide keeps its element when a page lands behind it instead of being rebuilt from its index. It is a prop of React, Vue, and Angular alike.
  • Images load before their slide. A slide further down is not mounted yet, because only the visible range is, as virtualization explains. So each new page's images are requested as soon as the page arrives and come from the browser cache when their slide mounts.
  • Keys and wheel are off on purpose. enableNavKeys listens on the whole window and enableWheel holds the page's scroll while the pointer is over the feed. Both suit a feed that fills the screen and fight a page that holds one, as this one does, so here they stay off and two buttons call prev() and next() on the Reel API instead. Swiping works either way.
  • Nothing outlives the feed. Each request carries an AbortController, aborted when the feed unmounts, so a late response never writes into a feed that is gone.

Next Steps