Pular para o conteúdo principal

Paginação REST

Listas expansíveis​

Caso você queira acrescentar resultados à sua lista existente, em vez de ir para outra página, Resource.getList.getPage pode ser usado, desde que paginationField tenha sido informado.

import { useController, useLoading } from '@data-client/react';
import { PostResource } from './Post';

export default function LoadMore({ cursor }: { cursor: string }) {
  const ctrl = useController();
  const [loadPage, isPending] = useLoading(
    () => ctrl.fetch(PostResource.getList.getPage, { cursor }),
    [cursor],
  );
  return (
    <center>
      <button onClick={loadPage} disabled={isPending}>
        {isPending ? '...' : 'Load more'}
      </button>
    </center>
  );
}
Resultado
Store▶

Não se esqueça de definir o paginationField do nosso Resource e o schema correto!

Post
export const PostResource = resource({
path: '/posts/:id',
schema: Post,
paginationField: 'cursor',
}).extend('getList', {
schema: { posts: new Collection([Post]), cursor: '' },
});

Demo de Issues do Github​

Nosso componente NextPage tem um handler de clique que chama RestEndpoint.getPage. Role até o final da prévia e clique em "Load more" para acrescentar a próxima página de issues.

Explore o exemplo github-app

More Demos

Usando RestEndpoint diretamente​

Aqui exploramos um exemplo do mundo real usando a lista de validators do cosmos.

Como validators têm apenas um Endpoint, usamos RestEndpoint em vez de resource. Usando Collections e paginationField, podemos chamar RestEndpoint.getPage para acrescentar a próxima página de validators à nossa lista.

import { useController, useLoading } from '@data-client/react';
import { getValidators } from './Validator';

export default function LoadMore({ next_key, limit }) {
  const ctrl = useController();
  const [handleLoadMore, isPending] = useLoading(
    () =>
      ctrl.fetch(getValidators.getPage, {
        'pagination.limit': limit,
        'pagination.key': next_key,
      }),
    [next_key, limit],
  );
  if (!next_key) return null;
  return (
    <center>
      <button onClick={handleLoadMore} disabled={isPending}>
        {isPending ? '...' : 'Load more'}
      </button>
    </center>
  );
}
Resultado
Store▶

Scroll infinito​

Como os comportamentos de UI variam muito, e as implementações variam conforme a plataforma (react-native ou web), vamos apenas supor que um componente Pagination foi construído e usa um callback para disparar a busca da próxima página. Na web, recomenda-se usar algo baseado em Intersection Observers

import { useSuspense, useController } from '@data-client/react';
import { PostResource } from 'resources/Post';
import Pagination from './Pagination';
import PostList from './PostList';

function NewsList() {
const { results, cursor } = useSuspense(PostResource.getList);
const ctrl = useController();

return (
<Pagination
onPaginate={() =>
ctrl.fetch(PostResource.getList.getPage, { cursor })
}
>
<PostList posts={results} />
</Pagination>
);
}

Tokens em headers HTTP​

Em alguns casos, os tokens de paginação virão embutidos em headers HTTP, em vez de fazerem parte do payload. Nesse caso, você precisará personalizar a função parseResponse() de getList para que os headers de paginação sejam incluídos no objeto do fetch.

Mostramos o getList personalizado abaixo. Todas as outras partes do exemplo anterior permanecem iguais.

Neste exemplo, o token de paginação é armazenado no header link.

import { Collection, Resource } from '@data-client/rest';

export const ArticleResource = resource({
path: '/articles/:id',
schema: Article,
}).extend(Base => ({
getList: Base.getList.extend({
schema: { results: [Article], link: '' },
async parseResponse(response: Response) {
const results = await Base.getList.parseResponse(response);
if (
(response.headers && response.headers.has('link')) ||
Array.isArray(results)
) {
return {
link: response.headers.get('link'),
results,
};
}
return results;
},
}),
}));

Organização do código​

Se grande parte da sua API compartilha uma paginação semelhante, você pode experimentar uma classe Endpoint personalizada que compartilhe essa lógica.

resources/PagingEndpoint.ts
import { Collection, RestEndpoint, type RestGenerics } from '@data-client/rest';

export class PagingEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
async parseResponse(response: Response) {
const results = await super.parseResponse(response);
if (
(response.headers && response.headers.has('link')) ||
Array.isArray(results)
) {
return {
link: response.headers.get('link'),
results,
};
}
return results;
}
}
resources/MyResource.ts
import { Collection, Entity, resource } from '@data-client/rest';

import { PagingEndpoint } from './PagingEndpoint';

export const MyResource = resource({
path: '/stuff/:id',
schema: MyEntity,
Endpoint: PagingEndpoint,
});