/**
* usePagination Hook
*
* Generic reusable pagination hook for managing page state and navigation.
* Provides page number, page size, skip offset calculation, and navigation handlers.
*
* @layer Presentation
*/
import { useState, useCallback, useMemo } from 'react';
/**
* Pagination Hook Options
*/
export interface UsePaginationOptions {
/** Initial page number (default: 1) */
initialPage?: number;
/** Initial page size (default: 20) */
initialPageSize?: number;
/** Default page size for reset (default: 20) */
defaultPageSize?: number;
}
/**
* Pagination Hook Return Type
*/
export interface UsePaginationReturn {
/** Current page number (1-indexed) */
currentPage: number;
/** Number of items per page */
pageSize: number;
/** Skip offset for API (0-indexed, calculated as (page - 1) * pageSize) */
skip: number;
/** Navigate to specific page */
setPage: (page: number) => void;
/** Change page size (resets to page 1) */
setPageSize: (size: number) => void;
/** Reset pagination to initial state */
resetPagination: () => void;
/** Navigate to next page */
nextPage: () => void;
/** Navigate to previous page */
prevPage: () => void;
/** Navigate to first page */
firstPage: () => void;
/** Navigate to last page (requires total items) */
lastPage: (totalItems: number) => void;
/** Check if on first page */
isFirstPage: boolean;
/** Check if on last page (requires total items) */
isLastPage: (totalItems: number) => boolean;
}
/**
* usePagination Hook
*
* Manages pagination state for tables and lists. Handles page navigation,
* page size changes, and calculates skip offset for skip-based API pagination.
*
* @param options - Pagination configuration options
* @returns Pagination state and navigation functions
*
* @example
* ```tsx
* function DataTable() {
* const pagination = usePagination({ defaultPageSize: 20 });
*
* const { data } = useQuery({
* queryKey: ['items', pagination.skip, pagination.pageSize],
* queryFn: () => fetchItems({
* skip: pagination.skip,
* limit: pagination.pageSize,
* }),
* });
*
* return (
*
* );
* }
* ```
*/
export function usePagination(options: UsePaginationOptions = {}): UsePaginationReturn {
const {
initialPage = 1,
initialPageSize = 20,
defaultPageSize = 20,
} = options;
// State
const [currentPage, setCurrentPage] = useState(initialPage);
const [pageSize, setPageSizeState] = useState(initialPageSize);
/**
* Calculate skip offset for API
* Skip is 0-indexed: skip = (page - 1) * pageSize
*/
const skip = useMemo(() => {
return (currentPage - 1) * pageSize;
}, [currentPage, pageSize]);
/**
* Navigate to specific page
*/
const setPage = useCallback((page: number) => {
if (page < 1) {
console.warn('[usePagination] Page number must be >= 1, clamping to 1');
setCurrentPage(1);
return;
}
setCurrentPage(page);
}, []);
/**
* Change page size and reset to page 1
* (changing page size usually requires going back to start)
*/
const setPageSize = useCallback((size: number) => {
if (size < 1) {
console.warn('[usePagination] Page size must be >= 1, clamping to 1');
setPageSizeState(1);
setCurrentPage(1);
return;
}
setPageSizeState(size);
setCurrentPage(1); // Reset to first page
}, []);
/**
* Reset pagination to initial state
*/
const resetPagination = useCallback(() => {
setCurrentPage(initialPage);
setPageSizeState(defaultPageSize);
}, [initialPage, defaultPageSize]);
/**
* Navigate to next page
*/
const nextPage = useCallback(() => {
setCurrentPage((prev) => prev + 1);
}, []);
/**
* Navigate to previous page (min page 1)
*/
const prevPage = useCallback(() => {
setCurrentPage((prev) => Math.max(1, prev - 1));
}, []);
/**
* Navigate to first page
*/
const firstPage = useCallback(() => {
setCurrentPage(1);
}, []);
/**
* Navigate to last page
* @param totalItems - Total number of items
*/
const lastPage = useCallback((totalItems: number) => {
const lastPageNum = Math.ceil(totalItems / pageSize);
setCurrentPage(lastPageNum > 0 ? lastPageNum : 1);
}, [pageSize]);
/**
* Check if on first page
*/
const isFirstPage = currentPage === 1;
/**
* Check if on last page
* @param totalItems - Total number of items
*/
const isLastPage = useCallback((totalItems: number) => {
const lastPageNum = Math.ceil(totalItems / pageSize);
return currentPage >= lastPageNum;
}, [currentPage, pageSize]);
return {
currentPage,
pageSize,
skip,
setPage,
setPageSize,
resetPagination,
nextPage,
prevPage,
firstPage,
lastPage,
isFirstPage,
isLastPage,
};
}