Курсорная пагинация
Все list-эндпоинты MineFlow используют курсорную пагинацию (не offset). Ответ всегда имеет форму { items, nextCursor }: серверный предел страни цы — до 200 элементов, а nextCursor указывает, откуда тянуть следующую страницу. nextCursor === null означает, что страниц больше нет.
Этот рецепт показывает, как читать списки страница-за-страницей и как стянуть весь набор сразу. За полным API хуков — гайд client-react, за низкоуровневым ядром — client-core.
Формат страницы
import type { CursorPage } from '@mineflow/client-core';
interface CursorPage<T> {
items: T[];
nextCursor: string | null; // null = последняя страница
}
CursorPage<T> экспортируется из @mineflow/client-core и реэкспортируется из @mineflow/client-react. Все list-хуки (useAssets, usePersonnel, useShiftReports, useNotifications, …) и сырые GET-запросы возвращают именно эту форму.
/api/v1Org-scope и multi-tenancy полностью на сервере (по JWT). В курсоре и query-параметрах organization_id не передаётся. Подробнее про конфиг клиента — в гайде client-react.
Списочные хуки отдают nextCursor
Концептуальные list-хуки client-react возвращают стандартный UseQueryResult, в data которого лежит { items, nextCursor }:
import { useAssets } from '@mineflow/client-react';
function AssetsList() {
const { data, isLoading, error } = useAssets({ status: 'operational', limit: 50 });
if (isLoading) return <Spinner />;
if (error) return <ErrorView error={error} />;
return (
<>
{data?.items.map((a) => (
<AssetRow key={a.id} asset={a} />
))}
{/* есть ли следующая страница */}
{data?.nextCursor && <LoadMore cursor={data.nextCursor} />}
</>
);
}
data.nextCursor — это всё, что нужно, чтобы решить, показывать ли «Загрузить ещё». Само значение курсора непрозрачно: не парси его, просто передавай обратно в следующий запрос.
Ручная подгрузка «Загрузить ещё»
Самый прямой способ — держать текущий курсор в стейте и запрашивать следующую страницу через дженерик-хук useApiQuery, склеивая items в приложении. Курсор передаётся в query-параметре cursor:
import { useState } from 'react';
import { useApiQuery, queryKeys } from '@mineflow/client-react';
import { unwrap } from '@mineflow/client-core';
function PersonnelPicker({ positionId }: { positionId: string }) {
const [cursor, setCursor] = useState<string | null>(null);
const [rows, setRows] = useState<Personnel[]>([]);
const page = useApiQuery(
[...queryKeys.personnel.all, 'paged', positionId, cursor],
async (c) =>
unwrap(
await c.GET('/api/v1/hr/personnel', {
params: { query: { positionId, limit: 50, ...(cursor ? { cursor } : {}) } },
}),
),
);
// накапливаем items по мере прихода страниц
useEffect(() => {
if (page.data) setRows((prev) => [...prev, ...page.data.items]);
}, [page.data]);
return (
<>
{rows.map((p) => (
<PersonRow key={p.id} person={p} />
))}
{page.data?.nextCursor && (
<button onClick={() => setCursor(page.data!.nextCursor)}>Загрузить ещё</button>
)}
</>
);
}
cursorПервую страницу запрашивай вообще без поля cursor (либо с cursor: undefined). Не передавай пустую строку или null — отдай query-объект, в котором ключа cursor нет.
queryKeys — единый источник ключей кэша TanStack Query. Подмешивай в ключ cursor (и фильтры), чтобы каждая страница кэшировалась отдельно. Подробнее про ключи — в гайде client-react.