import { useCallback, useEffect, useState } from "react";export type PaginatedResponse<Data, ExtraPayload extends Record<string, unknown>> = {data: Data[];totalCount: number;extra?: ExtraPayload}export type PaginationParams = {limit: number;offset: number;};export type UsePaginatedDataProps<Data, ExtraPayload extends Record<string, unknown>> = {queryFn: (params: PaginationParams) => Promise<PaginatedResponse<Data, ExtraPayload>>;limit?: number;initialOffset?: number;};export const usePaginatedData = <Data, ExtraPayload extends Record<string, unknown>>(params: UsePaginatedDataProps<Data, ExtraPayload>) => {const { queryFn, limit = 10, initialOffset = 0 } = params;const [offset, setOffset] = useState(initialOffset);const [pages, setPages] = useState<PaginatedResponse<Data, ExtraPayload>[]>([]);const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);const loadMore = useCallback(async () => {setIsFetchingNextPage(true);const nextData = await queryFn({limit,offset,});setPages(prev => [...prev, nextData]);setOffset(offset + limit);setIsFetchingNextPage(false);}, [queryFn, offset, limit]);useEffect(() => {loadMore();}, []);const data = pages.flatMap(page => page.data);const totalCount = pages[0]?.totalCount || null;const hasNextPage = totalCount !== null && data.length < totalCount;return {data,loadMore,hasNextPage,isFetchingNextPage,};};
Пример ответа от сервера:
GET /contacts?limit=5&offset=0
{"data": [{"phoneNumber": "579-620-1008 x091","firstName": "Daron","lastName": "Beier","id": "681b3a4c1f6fb92e663aed96"},{"phoneNumber": "648-821-9569 x43505","firstName": "Arjun","lastName": "Huels","id": "681b3a4c1f6fb92e663aed97"},{"phoneNumber": "(976) 309-4472 x6703","firstName": "Taryn","lastName": "Haley","id": "681b3a4c1f6fb92e663aed98"},{"phoneNumber": "(570) 564-0845 x804","firstName": "Rylan","lastName": "Bechtelar","id": "681b3a4c1f6fb92e663aed99"},{"phoneNumber": "425-254-1355 x157","firstName": "Stephen","lastName": "Cartwright","id": "681b3a4c1f6fb92e663aed9a"}],"totalCount": 38,"extra": {"currentUserContact": {"phoneNumber": "(570) 564-0845 x804","firstName": "Rylan","lastName": "Bechtelar","id": "681b3a4c1f6fb92e663aed99"}}}
Пример использования:
const CurrentUserBadge = () => (<span className="inline-flex px-2 py-[2px] bg-secondary text-body2 text-text rounded-lg">Me</span>);export const PaginationExample = () => {const {data,extra,hasNextPage,isFetchingNextPage,loadMore,} = usePaginatedData({queryFn: async ({ limit, offset }) => {const result = await getContacts(limit, offset);console.log(result);return result;},limit: 5,});const currentUserContact = extra?.currentUserContact;return (<div className="flex flex-col gap-2">{extra && (<span className="text-body1">Current user: {extra.currentUserContact.firstName} {extra.currentUserContact.lastName}</span>)}{data.map(contact => {const { firstName, lastName, id,phoneNumber } = contact;const isMe = currentUserContact?.id === id;return (<div key={id} className="flex flex-col"><span className="text-body2">{firstName} {lastName} {isMe && <CurrentUserBadge />}</span><span className="text-body2">{phoneNumber}</span></div>);})}{isFetchingNextPage && <span>Загрузка...</span>}{hasNextPage &&<button disabled={isFetchingNextPage} onClick={loadMore}>Загрузить еще</button>}</div>);};
Используя @tanstack/react-query
import { useInfiniteQuery } from '@tanstack/react-query';export type PaginatedResponse<Data, ExtraPayload extends Record<string, unknown>> = {data: Data[];totalCount: number;} & ExtraPayload;export type PaginationParams = {limit: number;offset: number;};export type UsePaginatedDataProps<Data, ExtraPayload extends Record<string, unknown>> = {queryKey: string[];queryFn: (params: PaginationParams) => Promise<PaginatedResponse<Data, ExtraPayload>>;limit?: number;initialPageParam?: number;enabled?: boolean;};export const usePaginatedData = <Data, ExtraPayload extends Record<string, unknown>>({queryKey,queryFn,limit = 6,initialPageParam = 0,enabled = true,}: UsePaginatedDataProps<Data, ExtraPayload>) => {const { data, ...rest } = useInfiniteQuery({queryKey,queryFn: ({ pageParam = initialPageParam }) =>queryFn({limit,offset: pageParam,}),enabled,initialPageParam,getNextPageParam: (curr, all, lastPageParam) => {return lastPageParam + limit;},});const allData = data?.pages?.reduce<Data[]>((acc, curr) => [...acc, ...(curr.data || [])], []) || [];const lastPage = data?.pages?.[data?.pages?.length - 1] || { totalCount: 0, data: null };const { totalCount, data: _data, ...extraPayload } = lastPage;return {...(extraPayload as ExtraPayload),...rest,hasNextPage: allData?.length < totalCount,data: allData,};};