跳到主要内容

Rest 分页

扩展列表​

如果你想把结果追加到现有列表中,而不是跳转到另一页,只要提供了 paginationField,就可以使用 Resource.getList.getPage。

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>
  );
}
结果
Store▶

别忘了为 Resource 定义 paginationField 以及正确的 schema!

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

Github Issues 演示​

NextPage 组件有一个点击处理函数,它会调用 RestEndpoint.getPage。滚动到预览底部,点击 "Load more" 即可追加下一页 issue。

探索 github-app 示例

更多演示

直接使用 RestEndpoint​

下面我们使用 cosmos 验证者列表来探索一个真实的示例。

由于验证者只有一个 Endpoint,我们使用 RestEndpoint 而不是 resource。借助 Collections 和 paginationField,我们可以调用 RestEndpoint.getPage,将下一页验证者追加到列表中。

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>
  );
}
结果
Store▶

无限滚动​

由于 UI 行为差异很大,实现方式也因平台(react-native 或 web)而异,这里我们只假设已经构建好了一个 Pagination 组件,它通过回调来触发下一页的获取。在 web 上,推荐使用基于 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>
);
}

HTTP 头中的 token​

在某些情况下,分页 token 会嵌在 HTTP 头中,而不是作为响应体的一部分。这时你需要为 getList 自定义 parseResponse() 函数,让分页头包含在获取到的对象中。

下面展示自定义的 getList。上述示例的其余部分保持不变。

在本例中,分页 token 存储在 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;
},
}),
}));

代码组织​

如果你的大部分 API 都采用类似的分页方式,可以尝试用一个自定义的 Endpoint 类来共享这段逻辑。

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,
});