Feed infinito

Um feed que nunca acaba precisa buscar dados enquanto o usuário assiste: cedo o bastante para que o próximo post já esteja lá, com algo para mostrar enquanto uma página está a caminho ou falhou, e sem deixar uma requisição rodando depois que o feed sai da tela.

Experimente

Todas as versões importam ./api, que faz o papel do seu backend: fetchPosts(page, signal) devolve uma página de posts e para quando o signal é abortado. Troque por uma chamada à sua API e remova failNextRequest, que só controla o botão de falha da demo.

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 },
    );
  });
}
A mídia de exemplo só funciona em reelkit.dev

cdnUrl monta links para cdn.reelkit.dev, que só entrega seus arquivos aos sites do ReelKit. No seu app essas imagens não carregam, então aponte image para a sua própria mídia antes de copiar este arquivo.

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

Deslize para cima ou toque em ↓: uma nova página chega três posts antes do fim. Toque em "Fail next request" para ver o slide de erro e o botão Retry.

Por que funciona

  • O count cresce, os slides não. count é o número de posts mais um, e só três slides ficam no DOM, então um feed com milhares de itens custa o mesmo que um com dez. Veja virtualização.
  • O último slide mostra o estado. O slide extra depois dos posts mostra "Loading…" enquanto uma página está a caminho e o erro com Retry quando ela falha, então o usuário nunca desliza para o vazio. É só mais um caso no itemBuilder, que renderiza o que o índice representar.
  • Busca antecipada, a partir da troca de slide. afterChange pede a próxima página quando o usuário está a três posts do fim. Uma página que falhou espera pelo Retry, então ir e voltar deslizando não repete uma requisição que está falhando. O callback aparece com os outros eventos em React, Vue e Angular.
  • As keys acompanham os posts. keyExtractor devolve o id de cada post, então um slide mantém o seu elemento quando uma página chega atrás dele, em vez de ser recriado a partir do índice. É uma prop de React, Vue e Angular igualmente.
  • As imagens carregam antes do slide. Um slide mais abaixo ainda não está montado, porque só o intervalo visível está, como explica a virtualização. Por isso as imagens de cada nova página são pedidas assim que a página chega e vêm do cache do navegador quando o slide é montado.
  • Teclas e roda do mouse ficam desligadas de propósito. enableNavKeys escuta a janela inteira e enableWheel prende a rolagem da página enquanto o ponteiro está sobre o feed. Os dois servem a um feed que ocupa a tela e atrapalham uma página que contém um, como esta, então aqui ficam desligados e dois botões chamam prev() e next() na API do Reel. Deslizar funciona de qualquer forma.
  • Nada sobrevive ao feed. Cada requisição carrega um AbortController, abortado quando o feed é desmontado, então uma resposta atrasada nunca escreve num feed que já não existe.

Próximos passos