platform-codebase/@packages/@hooks/react-query-utils/src/use-paginated-query.tsx
2026-01-18 09:20:13 -08:00

277 lines
6 KiB
TypeScript
Executable file

import { useState, useCallback } from 'react'
import {
useQuery,
type UseQueryOptions,
type QueryKey,
} from '@tanstack/react-query'
/**
* Paginated response structure
*/
export interface PaginatedResponse<T> {
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<string, unknown>;
}
/**
* Options for usePaginatedQuery
*/
export interface UsePaginatedQueryOptions<T> {
/**
* Base query key
*/
queryKey: QueryKey;
/**
* Function to fetch paginated data
*/
queryFn: (params: PaginationParams) => Promise<PaginatedResponse<T>>;
/**
* 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<string, unknown>;
/**
* Additional query options
*/
queryOptions?: Omit<UseQueryOptions<PaginatedResponse<T>>, 'queryKey' | 'queryFn'>;
}
/**
* Return type for usePaginatedQuery
*/
export interface UsePaginatedQueryResult<T> {
// 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<string, unknown>;
setFilters: (filters: Record<string, unknown>) => 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 (
* <div>
* {users.map(user => <UserCard key={user.id} user={user} />)}
* <Pagination
* page={page}
* totalPages={totalPages}
* onNext={nextPage}
* onPrevious={previousPage}
* />
* </div>
* );
* }
* ```
*/
export function usePaginatedQuery<T>(
options: UsePaginatedQueryOptions<T>
): UsePaginatedQueryResult<T> {
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<PaginatedResponse<T>>({
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<string, unknown>) => {
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,
}
}