@guanriyue/decurl/pagination

@guanriyue/decurl/pagination 提供基于 Search Fields 的分页状态 hook,用于读取和更新 pagepageSize,并处理分页场景中的常见联动行为。

import { useSearchPagination } from '@guanriyue/decurl/pagination';

useSearchPagination

调用形式

useSearchPagination(): UseSearchPaginationResult
useSearchPagination(options?): UseSearchPaginationResult
useSearchPagination(fields, options?): UseSearchPaginationResult
调用说明
useSearchPagination()使用内置分页 Fields 和默认行为。
useSearchPagination(options?)使用内置分页 Fields,并配置分页行为。
useSearchPagination(fields, options?)使用自定义分页 Fields,可选配置分页行为。

参数

参数类型必填说明
fieldsSearchPaginationFields自定义 pagepageSize Fields。
optionsUseSearchPaginationOptionspage size 变化时的分页行为。

传入单个对象时,如果对象同时包含 pagepageSize,会被识别为 SearchPaginationFields;否则会被识别为 UseSearchPaginationOptions

默认模型

不传入自定义 Fields 时,Hook 使用 pagepageSize 两个 URL search params:

Field默认值Decode 规则
page1trim 后必须是从 1 开始的十进制正安全整数。
pageSize10必须满足相同的正整数规则,并包含在默认 page size options 中。

011.51e2、负数和超过安全整数范围的值都会 decode 失败,并回退到对应的默认值。

useSearchPagination.fields 暴露默认分页模型使用的 Fields:

useSearchPagination.fields
// { page, pageSize }

当分页 Fields 还需要和筛选、排序等字段一起交给 useSearchValues 时,可以复用它们:

const listFields = {
  ...useSearchPagination.fields,
  keyword: keywordField,
  order: orderField,
};

useSearchPagination.pageSizeOptions 是默认模型对应的每页条数选项:

useSearchPagination.pageSizeOptions
// [10, 20, 50, 100]

它既参与默认 pageSize Field 的 decode,也可以用于渲染默认分页模型的 page size 选择器。

自定义 Fields

传入 SearchPaginationFields 可以自定义 URL key、默认值和 codec 规则:

import { field } from '@guanriyue/decurl/codec';
import {
  elementOf,
  min,
  pipe,
  shape,
  toNumber,
  trim,
} from '@guanriyue/decurl/decode';
import type { SearchPaginationFields } from '@guanriyue/decurl/pagination';
import { useSearchPagination } from '@guanriyue/decurl/pagination';

const pageSizeOptions = [25, 50, 100];

const paginationFields = {
  page: field({
    name: 'p',
    decode: pipe(trim, shape.integer, toNumber, min(1)),
    defaultValue: 1,
  }),
  pageSize: field({
    name: 'size',
    decode: pipe(
      trim,
      shape.integer,
      toNumber,
      elementOf(pageSizeOptions),
    ),
    defaultValue: 25,
  }),
} satisfies SearchPaginationFields;

const pagination = useSearchPagination(paginationFields);

自定义 Fields 每次 decode 后都必须满足 Pagination 的算法不变量:

  • page 是从 1 开始的正安全整数,默认值应为 1
  • pageSize 是大于 0 的正安全整数,并提供确定的合法默认值。
  • 固定 page size 必须大于 0
  • 不支持 0-based page。

这些约束由 FieldCodec 保证。Pagination setter 不会建立独立于 codec 的输入校验规则。

自定义 Fields 不会消费 useSearchPagination.pageSizeOptions,也不会从 codec 反推出 page size 选项。页面需要渲染选择器时,应使用与自定义 codec 对应的静态数据,例如上面的 pageSizeOptions

Options

pageSizeChangeStrategy 决定调用 setPageSize 时如何同步更新页码:

默认值说明
'reset'page 设置为 1
'preserve-offset'尽量让原页第一条数据仍处于新的页码范围内。
const pagination = useSearchPagination({
  pageSizeChangeStrategy: 'preserve-offset',
});

preserve-offset 使用下面的计算方式:

Math.floor(((page - 1) * pageSize) / nextPageSize) + 1

其中 nextPageSize 是传给 setPageSize 的原始值。计算完成后,pagepageSize patch 仍分别交给对应的 FieldCodec。

返回值

type UseSearchPaginationResult = {
  page: number
  pageSize: number
  setPage: (page: number, options?: SearchNavigateOptions) => void
  resetPage: () => void
  setPageSize: (
    pageSize: number,
    options?: SearchNavigateOptions,
  ) => void
  setPagination: (
    patch: {
      page?: number | null | undefined
      pageSize?: number | null | undefined
    },
    options?: SearchNavigateOptions,
  ) => void
  preventOverflow: (
    totalSource:
      | number
      | null
      | undefined
      | { total?: number | null | undefined },
  ) => void
}

分页状态

返回值类型说明
pagenumber当前 decode 后的页码。
pageSizenumber当前 decode 后的每页条数。

状态更新

方法签名说明
setPage(page, options?) => void提交新的语义页码。
resetPage() => void提交 page: 1
setPageSize(pageSize, options?) => void提交每页条数,并根据策略同步提交页码。
setPagination(patch, options?) => void一次提交 page 和 / 或 pageSize patch。

setPage 提交的值最终如何 encode、是否从 URL 中省略,以及重新 decode 后得到什么结果,均由 page FieldCodec 决定。

resetPage 始终提交 page: 1,不接收 Navigate Options。最终 URL 表示仍由 page FieldCodec 决定。

setPageSize 会按照 pageSizeChangeStrategy 同时计算 page patch。当传入值与当前 decode 后的 pageSize 相同时,不提交更新。

setPagination 本质上是分页 Fields 对应的 setSearchValues,只是使用了更符合分页语义的名称。它保留相同的 patch 编码行为,但公开类型只接受直接 patch,不提供 updater:

pagination.setPagination({
  page: 3,
  pageSize: 20,
});

setPagination 不应用 pageSizeChangeStrategy。同时提供两个字段时,它们会按原值分别交给对应的 FieldCodec;如果需要 page size 联动,应使用 setPageSize

省略属性表示对应 Field 不参与本次 patch。显式传入 nullundefined 则会把这个值交给 FieldCodec,通常用于删除对应的 URL key:

pagination.setPagination({});
pagination.setPagination({ page: undefined });

setPagesetPageSizesetPagination 的 Navigate Options 与 setSearchValues 完全相同。

越界恢复

preventOverflow(totalSource) 根据当前 decode 后的 pageSize 计算最大页码,并在当前 page 更大时提交最大页码:

const maxPage = Math.max(1, Math.ceil(total / pageSize));

可以传入数字,也可以直接传入包含 total 的接口结果:

pagination.preventOverflow(result.total);
pagination.preventOverflow(result);

合法的 total 必须是大于或等于 0 的安全整数。nullundefined、负数、小数、NaNInfinity 及对象中的非法 total 都表示没有可用边界,不会提交修正。total=0 时最大页码仍为 1

preventOverflow 不判断数据来源是否可信。调用方应确认 total 属于当前查询条件,并且请求结果已经可以被当前页面接受后再调用。

它是一次性的恢复操作,不是持续维护分页不变量的 guard。它不保证在请求库复用缓存或 dedupe 后再次执行,也不替代分页组件对上一页、下一页和页码输入的边界限制。

请求缓存、旧数据与修正时机之间的取舍见 理解分页越界

引用稳定性

setPageresetPagesetPageSizesetPaginationpreventOverflow 会在组件重新渲染后保持稳定引用。

createUseSearchPagination

createUseSearchPagination(options: {
  useSearchValues: <TDefinition extends RecordCodec>(
    fields: TDefinition,
  ) => UseSearchValuesResult<TDefinition>
}): UseSearchPagination

createUseSearchPagination 使用指定的 useSearchValues 创建分页 hook。

它可以让 Pagination 消费指定的 useSearchValues 实现。例如和 provided hooks 组合:

import { createUseSearchPagination } from '@guanriyue/decurl/pagination';
import { useProvidedSearchValues } from '@guanriyue/decurl/provided';

const useSearchPagination = createUseSearchPagination({
  useSearchValues: useProvidedSearchValues,
});

返回值是 UseSearchPagination。它具有与默认 hook 相同的调用形式、返回值以及 fieldspageSizeOptions 静态属性。

Types

SearchPaginationFields

type SearchPaginationFields = {
  page: SingleRequiredFieldCodec<number>
  pageSize: SingleRequiredFieldCodec<number>
}
字段类型说明
pageSingleRequiredFieldCodec<number>Decode 后提供确定的数字页码。Pagination 要求它是从 1 开始的正安全整数。
pageSizeSingleRequiredFieldCodec<number>Decode 后提供确定的每页条数。Pagination 要求它是大于 0 的正安全整数。

SearchPaginationPageSizeChangeStrategy

type SearchPaginationPageSizeChangeStrategy =
  | 'reset'
  | 'preserve-offset'
说明
'reset'page size 变化时把页码重置为 1
'preserve-offset'根据当前 page、pageSize 和新的 pageSize 重新计算页码。

UseSearchPaginationOptions

type UseSearchPaginationOptions = {
  pageSizeChangeStrategy?: SearchPaginationPageSizeChangeStrategy
}
字段类型必填默认值说明
pageSizeChangeStrategySearchPaginationPageSizeChangeStrategy'reset'setPageSize 如何同步更新页码。

UseSearchPaginationResult

字段类型说明
pagenumber当前 decode 后的页码。
pageSizenumber当前 decode 后的每页条数。
setPage(page: number, options?: SearchNavigateOptions) => void提交新的页码。
resetPage() => void提交 page: 1
setPageSize(pageSize: number, options?: SearchNavigateOptions) => void提交 page size 并应用页码联动策略。
setPagination(patch, options?: SearchNavigateOptions) => void提交直接分页 patch,不提供 updater。
preventOverflow(totalSource) => void根据可信 total 执行一次越界恢复。

其中 setPagination 的 patch 为:

{
  page?: number | null | undefined
  pageSize?: number | null | undefined
}

preventOverflowtotalSource 为:

number
  | null
  | undefined
  | { total?: number | null | undefined }

UseSearchPagination

type UseSearchPagination = {
  (): UseSearchPaginationResult
  (options?: UseSearchPaginationOptions): UseSearchPaginationResult
  (
    fields: SearchPaginationFields,
    options?: UseSearchPaginationOptions,
  ): UseSearchPaginationResult
  fields: SearchPaginationFields
  pageSizeOptions: readonly number[]
}
字段类型说明
调用签名overload使用默认或自定义 Fields 创建分页状态。
fieldsSearchPaginationFields默认分页模型使用的 Fields。
pageSizeOptionsreadonly number[]默认分页模型使用的 page size 选项。