import { useState, useCallback } from 'react' import { useQuery, type UseQueryOptions, type QueryKey, } from '@tanstack/react-query' /** * Paginated response structure */ export interface PaginatedResponse { data: T[]; total: number; page: number; pageSize: number; totalPages: number; } /** * Pagination parameters */ export interface PaginationParams { page: number; pageSize: number; sortBy?: string; sortOrder?: 'asc' | 'desc'; filters?: Record; } /** * Options for usePaginatedQuery */ export interface UsePaginatedQueryOptions { /** * Base query key */ queryKey: QueryKey; /** * Function to fetch paginated data */ queryFn: (params: PaginationParams) => Promise>; /** * Initial page number * @default 1 */ initialPage?: number; /** * Initial page size * @default 10 */ initialPageSize?: number; /** * Initial sort field */ initialSortBy?: string; /** * Initial sort order * @default 'asc' */ initialSortOrder?: 'asc' | 'desc'; /** * Initial filters */ initialFilters?: Record; /** * Additional query options */ queryOptions?: Omit>, 'queryKey' | 'queryFn'>; } /** * Return type for usePaginatedQuery */ export interface UsePaginatedQueryResult { // Data data: T[]; total: number; page: number; pageSize: number; totalPages: number; // Query state isLoading: boolean; isError: boolean; error: Error | null; isFetching: boolean; // Pagination controls nextPage: () => void; previousPage: () => void; goToPage: (page: number) => void; setPageSize: (size: number) => void; canNextPage: boolean; canPreviousPage: boolean; // Sorting sortBy: string | undefined; sortOrder: 'asc' | 'desc'; setSorting: (field: string, order?: 'asc' | 'desc') => void; // Filtering filters: Record; setFilters: (filters: Record) => void; setFilter: (key: string, value: unknown) => void; clearFilters: () => void; // Refetch refetch: () => void; } /** * Hook for paginated queries with built-in pagination controls * * Provides pagination state management, sorting, filtering, and * automatic query key management. * * @example * ```typescript * function UserList() { * const { * data: users, * isLoading, * page, * totalPages, * nextPage, * previousPage, * setSorting, * setFilters, * } = usePaginatedQuery({ * queryKey: ['users'], * queryFn: ({ page, pageSize, sortBy, sortOrder, filters }) => * apiClient.get('/users', { * params: { page, pageSize, sortBy, sortOrder, ...filters }, * }), * initialPageSize: 20, * initialSortBy: 'createdAt', * initialSortOrder: 'desc', * }); * * return ( *
* {users.map(user => )} * *
* ); * } * ``` */ export function usePaginatedQuery( options: UsePaginatedQueryOptions ): UsePaginatedQueryResult { const { queryKey, queryFn, initialPage = 1, initialPageSize = 10, initialSortBy, initialSortOrder = 'asc', initialFilters = {}, queryOptions, } = options // Pagination state const [page, setPage] = useState(initialPage) const [pageSize, setPageSize] = useState(initialPageSize) // Sorting state const [sortBy, setSortBy] = useState(initialSortBy) const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(initialSortOrder) // Filtering state const [filters, setFilters] = useState(initialFilters) // Query const query = useQuery>({ queryKey: [...queryKey, { page, pageSize, sortBy, sortOrder, filters }], queryFn: () => queryFn({ page, pageSize, sortBy, sortOrder, filters }), ...queryOptions, }) // Pagination controls const nextPage = useCallback(() => { if (query.data && page < query.data.totalPages) { setPage((prev) => prev + 1) } }, [page, query.data]) const previousPage = useCallback(() => { if (page > 1) { setPage((prev) => prev - 1) } }, [page]) const goToPage = useCallback((newPage: number) => { if (newPage >= 1 && query.data && newPage <= query.data.totalPages) { setPage(newPage) } }, [query.data]) const handleSetPageSize = useCallback((size: number) => { setPageSize(size) setPage(1) // Reset to first page when changing page size }, []) // Sorting controls const setSorting = useCallback((field: string, order: 'asc' | 'desc' = 'asc') => { setSortBy(field) setSortOrder(order) setPage(1) // Reset to first page when sorting changes }, []) // Filtering controls const handleSetFilters = useCallback((newFilters: Record) => { setFilters(newFilters) setPage(1) // Reset to first page when filters change }, []) const setFilter = useCallback((key: string, value: unknown) => { setFilters((prev) => ({ ...prev, [key]: value })) setPage(1) }, []) const clearFilters = useCallback(() => { setFilters({}) setPage(1) }, []) return { // Data data: query.data?.data ?? [], total: query.data?.total ?? 0, page: query.data?.page ?? page, pageSize: query.data?.pageSize ?? pageSize, totalPages: query.data?.totalPages ?? 0, // Query state isLoading: query.isLoading, isError: query.isError, error: query.error, isFetching: query.isFetching, // Pagination controls nextPage, previousPage, goToPage, setPageSize: handleSetPageSize, canNextPage: !!query.data && page < query.data.totalPages, canPreviousPage: page > 1, // Sorting sortBy, sortOrder, setSorting, // Filtering filters, setFilters: handleSetFilters, setFilter, clearFilters, // Refetch refetch: query.refetch, } }