feat: add custom hooks for common patterns
- Add useLoadingState hook for managing loading states - Add useDebounce hook for debouncing values/callbacks - Add useApiCall hook for API call management with error handling - Add usePagination hook for pagination logic - Create centralized hooks/index.ts for exports - Reduce code duplication across components
This commit is contained in:
5
src/hooks/index.ts
Normal file
5
src/hooks/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Export all custom hooks from a single entry point
|
||||||
|
export { useLoadingState } from './useLoadingState';
|
||||||
|
export { useDebounce, useDebouncedCallback } from './useDebounce';
|
||||||
|
export { useApiCall } from './useApiCall';
|
||||||
|
export { usePagination } from './usePagination';
|
116
src/hooks/useApiCall.ts
Normal file
116
src/hooks/useApiCall.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface ApiCallOptions {
|
||||||
|
onSuccess?: (data: any) => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
showErrorToast?: boolean;
|
||||||
|
showSuccessToast?: boolean;
|
||||||
|
successMessage?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiCallState<T> {
|
||||||
|
data: T | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
call: (...args: any[]) => Promise<T | null>;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for making API calls with consistent error handling and loading states
|
||||||
|
* Includes automatic toast notifications and cleanup on unmount
|
||||||
|
*/
|
||||||
|
export function useApiCall<T>(
|
||||||
|
apiFunction: (...args: any[]) => Promise<T>,
|
||||||
|
options: ApiCallOptions = {}
|
||||||
|
): ApiCallState<T> {
|
||||||
|
const [data, setData] = useState<T | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
const isMountedRef = useRef(true);
|
||||||
|
|
||||||
|
const {
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
showErrorToast = true,
|
||||||
|
showSuccessToast = false,
|
||||||
|
successMessage = 'Operation completed successfully',
|
||||||
|
errorMessage
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const call = useCallback(
|
||||||
|
async (...args: any[]): Promise<T | null> => {
|
||||||
|
try {
|
||||||
|
// Cancel any pending request
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new abort controller
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const result = await apiFunction(...args);
|
||||||
|
|
||||||
|
// Only update state if component is still mounted
|
||||||
|
if (!isMountedRef.current) return null;
|
||||||
|
|
||||||
|
setData(result);
|
||||||
|
|
||||||
|
if (showSuccessToast) {
|
||||||
|
// TODO: Implement toast notification
|
||||||
|
console.log('Success:', successMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess?.(result);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore aborted requests
|
||||||
|
if (err instanceof Error && err.name === 'AbortError') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update state if component is still mounted
|
||||||
|
if (!isMountedRef.current) return null;
|
||||||
|
|
||||||
|
const error = err instanceof Error ? err : new Error('An error occurred');
|
||||||
|
setError(error);
|
||||||
|
|
||||||
|
if (showErrorToast) {
|
||||||
|
// TODO: Implement toast notification
|
||||||
|
console.error('Error:', errorMessage || error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
onError?.(error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[apiFunction, onSuccess, onError, showErrorToast, showSuccessToast, successMessage, errorMessage]
|
||||||
|
);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setData(null);
|
||||||
|
setError(null);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { data, isLoading, error, call, reset };
|
||||||
|
}
|
48
src/hooks/useDebounce.ts
Normal file
48
src/hooks/useDebounce.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useEffect, useState, useRef } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook that debounces a value
|
||||||
|
* Useful for search inputs and reducing API calls
|
||||||
|
*/
|
||||||
|
export function useDebounce<T>(value: T, delay: number): T {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook that returns a debounced callback
|
||||||
|
* The callback will only be invoked after the delay has passed since the last call
|
||||||
|
*/
|
||||||
|
export function useDebouncedCallback<T extends (...args: any[]) => any>(
|
||||||
|
callback: T,
|
||||||
|
delay: number
|
||||||
|
): T {
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const callbackRef = useRef(callback);
|
||||||
|
|
||||||
|
// Update callback ref on each render to avoid stale closures
|
||||||
|
callbackRef.current = callback;
|
||||||
|
|
||||||
|
return useRef(
|
||||||
|
((...args: Parameters<T>) => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
callbackRef.current(...args);
|
||||||
|
}, delay);
|
||||||
|
}) as T
|
||||||
|
).current;
|
||||||
|
}
|
48
src/hooks/useLoadingState.ts
Normal file
48
src/hooks/useLoadingState.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
interface LoadingState<T> {
|
||||||
|
data: T | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
execute: (...args: any[]) => Promise<T>;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for managing loading states with error handling
|
||||||
|
* Reduces boilerplate code for async operations
|
||||||
|
*/
|
||||||
|
export function useLoadingState<T>(
|
||||||
|
asyncFunction: (...args: any[]) => Promise<T>
|
||||||
|
): LoadingState<T> {
|
||||||
|
const [data, setData] = useState<T | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
const execute = useCallback(
|
||||||
|
async (...args: any[]): Promise<T> => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const result = await asyncFunction(...args);
|
||||||
|
setData(result);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
const error = err instanceof Error ? err : new Error('An error occurred');
|
||||||
|
setError(error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[asyncFunction]
|
||||||
|
);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setData(null);
|
||||||
|
setError(null);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { data, isLoading, error, execute, reset };
|
||||||
|
}
|
123
src/hooks/usePagination.ts
Normal file
123
src/hooks/usePagination.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
|
|
||||||
|
interface PaginationOptions {
|
||||||
|
initialPage?: number;
|
||||||
|
initialPageSize?: number;
|
||||||
|
pageSizeOptions?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaginationResult<T> {
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
totalItems: number;
|
||||||
|
paginatedData: T[];
|
||||||
|
goToPage: (page: number) => void;
|
||||||
|
nextPage: () => void;
|
||||||
|
previousPage: () => void;
|
||||||
|
setPageSize: (size: number) => void;
|
||||||
|
canGoNext: boolean;
|
||||||
|
canGoPrevious: boolean;
|
||||||
|
pageRange: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for handling pagination logic
|
||||||
|
* Returns paginated data and pagination controls
|
||||||
|
*/
|
||||||
|
export function usePagination<T>(
|
||||||
|
data: T[],
|
||||||
|
options: PaginationOptions = {}
|
||||||
|
): PaginationResult<T> {
|
||||||
|
const {
|
||||||
|
initialPage = 1,
|
||||||
|
initialPageSize = 10,
|
||||||
|
pageSizeOptions: _pageSizeOptions = [10, 25, 50, 100]
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const [currentPage, setCurrentPage] = useState(initialPage);
|
||||||
|
const [pageSize, setPageSize] = useState(initialPageSize);
|
||||||
|
|
||||||
|
const totalItems = data.length;
|
||||||
|
const totalPages = Math.ceil(totalItems / pageSize);
|
||||||
|
|
||||||
|
// Calculate paginated data
|
||||||
|
const paginatedData = useMemo(() => {
|
||||||
|
const startIndex = (currentPage - 1) * pageSize;
|
||||||
|
const endIndex = startIndex + pageSize;
|
||||||
|
return data.slice(startIndex, endIndex);
|
||||||
|
}, [data, currentPage, pageSize]);
|
||||||
|
|
||||||
|
// Navigation functions
|
||||||
|
const goToPage = useCallback((page: number) => {
|
||||||
|
setCurrentPage(Math.max(1, Math.min(page, totalPages)));
|
||||||
|
}, [totalPages]);
|
||||||
|
|
||||||
|
const nextPage = useCallback(() => {
|
||||||
|
goToPage(currentPage + 1);
|
||||||
|
}, [currentPage, goToPage]);
|
||||||
|
|
||||||
|
const previousPage = useCallback(() => {
|
||||||
|
goToPage(currentPage - 1);
|
||||||
|
}, [currentPage, goToPage]);
|
||||||
|
|
||||||
|
const handleSetPageSize = useCallback((size: number) => {
|
||||||
|
setPageSize(size);
|
||||||
|
// Reset to first page when page size changes
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Generate page range for pagination UI
|
||||||
|
const pageRange = useMemo(() => {
|
||||||
|
const range: number[] = [];
|
||||||
|
const maxVisible = 7; // Maximum number of page buttons to show
|
||||||
|
|
||||||
|
if (totalPages <= maxVisible) {
|
||||||
|
// Show all pages if total is less than max
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
range.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Always show first page
|
||||||
|
range.push(1);
|
||||||
|
|
||||||
|
if (currentPage > 3) {
|
||||||
|
range.push(-1); // Ellipsis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show pages around current page
|
||||||
|
const start = Math.max(2, currentPage - 1);
|
||||||
|
const end = Math.min(totalPages - 1, currentPage + 1);
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
range.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage < totalPages - 2) {
|
||||||
|
range.push(-1); // Ellipsis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show last page
|
||||||
|
if (totalPages > 1) {
|
||||||
|
range.push(totalPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return range;
|
||||||
|
}, [currentPage, totalPages]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
totalItems,
|
||||||
|
paginatedData,
|
||||||
|
goToPage,
|
||||||
|
nextPage,
|
||||||
|
previousPage,
|
||||||
|
setPageSize: handleSetPageSize,
|
||||||
|
canGoNext: currentPage < totalPages,
|
||||||
|
canGoPrevious: currentPage > 1,
|
||||||
|
pageRange
|
||||||
|
};
|
||||||
|
}
|
Reference in New Issue
Block a user