platform-codebase/@packages/@hooks/react-query-utils/src/use-paginated-query.tsx
Quinn Ftw bb7f4dda2b feat(eslint): integrate global DRY ESLint packages across @packages
- Configure 12 @packages to use global @eslint/config-base and @eslint/config-react
- Update ESLint config path syntax to use node_modules paths
- Add ESLint dependencies to React packages (messaging-hooks, react-query-utils,
  websocket-client, analytics-client)
- Fix duplicate exports in @core/types (remove redundant re-exports)
- Auto-fix import order issues across all packages
- Add ESLint config for status-dashboard/server extending @eslint/config-base
- Migrate service-registry to @nestjs/bootstrap and @nestjs/health packages
- Integrate @nestjs/auth decorators (@Public, @CurrentUser) into auth system
- Fix FlexibleAuthGuard tests (add missing getAllAndOverride mock)
- Relax strict type-checking rules in base config for existing code

Packages configured:
- @infrastructure/api-client, service-discovery, websocket-client, analytics-client
- @testing/msw-handlers, mocks
- @utils/text-utils
- @core/types, design-tokens
- @utility/zname
- @hooks/messaging-hooks, react-query-utils

All packages now pass ESLint with 0 errors (warnings only).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 19:38:01 -08:00

277 lines
6 KiB
TypeScript

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,
}
}