無限フィード

終わりのないフィードは、見ているあいだに次を取得しなければなりません。次の投稿が届く前に視聴者が追いつかないよう早めに読み込み、ページを待っているあいだや失敗したときには何かを表示し、フィードがなくなったあとにリクエストを残さないことが必要です。

試してみる

どのバージョンも ./api をインポートしています。これはバックエンドの代わりで、fetchPosts(page, signal) は 1 ページ分の投稿を返し、シグナルが中断されると止まります。お使いの API の呼び出しに置き換え、デモの失敗スイッチを動かすためだけの failNextRequest は削除してください。

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 },
    );
  });
}
サンプルのメディアは reelkit.dev でしか使えません

cdnUrl が作るのは cdn.reelkit.dev へのリンクで、このサーバーは ReelKit のサイトにしかファイルを返しません。お使いのアプリでは画像を読み込めないので、このファイルをコピーする前に image を自分のメディアに向けてください。

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

上へスワイプするか ↓ を押してください。終わりの 3 件手前で次のページが届きます。「Fail next request」を押すと、エラーのスライドと Retry を確認できます。

仕組み

  • 件数は増えても、スライドは増えません。 count は投稿数に 1 を足した値で、DOM にあるスライドは常に 3 枚だけなので、数千件のフィードも 10 件のフィードと同じ負荷で済みます。仮想化を参照してください。
  • 最後のスライドは状態の表示です。 投稿の後ろにある 1 枚は、ページを待っているあいだは「Loading…」を、失敗したときはエラーと Retry を表示するので、スワイプした先が空になることはありません。インデックスが表すものを描画する itemBuilder の、ケースが 1 つ増えただけです。
  • スライドの切り替えから先読みします。 afterChange は、終わりまで 3 件になった時点で次のページを要求します。失敗したページは Retry を待つので、行ったり来たりスワイプしても失敗するリクエストを繰り返しません。このコールバックは、React、Vue、Angular のほかのイベントと一緒に載っています。
  • キーは投稿に付いていきます。 keyExtractor は各投稿の id を返すので、後ろにページが追加されてもスライドは要素を保ち、インデックスから作り直されることはありません。React、Vue、Angular のどれでも使えるプロパティです。
  • 画像はスライドより先に読み込みます。 仮想化の説明どおり、マウントされるのは表示範囲だけなので、先のスライドはまだ存在しません。そこで新しいページが届いた時点でその画像を要求しておき、スライドがマウントされたときにはブラウザーのキャッシュから表示します。
  • キー操作とホイールはあえてオフです。 enableNavKeys はウィンドウ全体で入力を受け取り、enableWheel はポインターがフィードの上にあるあいだページのスクロールを止めます。どちらも画面いっぱいのフィードには合いますが、このようにページの中に置いたフィードではページとぶつかるので、ここではオフにして、代わりに 2 つのボタンで Reel API の prev() と next() を呼びます。スワイプはどちらでも使えます。
  • フィードより長く生きるものはありません。 各リクエストは AbortController を持ち、フィードのアンマウント時に中断されるので、遅れて届いたレスポンスがなくなったフィードに書き込むことはありません。

次のステップ