Saltar al contenido principal

Paginación REST

Listas expandibles​

Si quieres añadir resultados a tu lista existente, en lugar de pasar a otra página, puedes usar Resource.getList.getPage siempre que se haya proporcionado paginationField.

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▶

¡No olvides definir el paginationField de nuestro Resource y el schema correcto!

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

Demo de issues de Github​

Nuestro componente NextPage tiene un manejador de clic que llama a RestEndpoint.getPage. Desplázate hasta el final de la vista previa y haz clic en "Load more" para añadir la siguiente página de issues.

Explora el ejemplo github-app

More Demos

Usar RestEndpoint directamente​

Aquí exploramos un ejemplo del mundo real usando la lista de validadores de cosmos.

Como los validadores solo tienen un Endpoint, usamos RestEndpoint en lugar de resource. Con Collections y paginationField, podemos llamar a RestEndpoint.getPage para añadir la siguiente página de validadores a nuestra 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▶

Desplazamiento infinito​

Como los comportamientos de la interfaz varían mucho, y las implementaciones cambian según la plataforma (react-native o web), asumiremos que existe un componente Pagination que usa un callback para activar la obtención de la siguiente página. En la web, se recomienda usar algo basado en 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 en las cabeceras HTTP​

En algunos casos los tokens de paginación vendrán incrustados en las cabeceras HTTP, en lugar de formar parte del payload. En este caso tendrás que personalizar la función parseResponse() de getList para que las cabeceras de paginación se incluyan en el objeto de fetch.

A continuación mostramos el getList personalizado. El resto de las partes del ejemplo anterior permanecen igual.

En este ejemplo, el token de paginación se almacena en la cabecera 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;
},
}),
}));

Organización del código​

Si gran parte de tu API comparte una paginación similar, puedes probar una clase Endpoint personalizada que comparta esta 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,
});