import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { DEBOUNCE_SEARCH, DEFAULT_PAGE, DEFAULT_PAGE_SIZE } from '../constants'; interface PaginatedResult { items: T[]; totalCount: number; } type FetchFn = ( params: Record, ) => Promise>; export function usePaginatedList( fetchFn: FetchFn, extraParams: Record = {}, ) { const [rows, setRows] = useState([]); const [search, setSearch] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [page, setPage] = useState(DEFAULT_PAGE); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const [totalCount, setTotalCount] = useState(0); const [loading, setLoading] = useState(true); const [err, setErr] = useState(''); const debounceRef = useRef | null>(null); const extraKey = JSON.stringify(extraParams); useEffect(() => { if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { setDebouncedSearch(search); setPage(1); }, DEBOUNCE_SEARCH); return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, [search]); const reload = useCallback(() => { setLoading(true); setErr(''); fetchFn({ search: debouncedSearch, page, pageSize, ...extraParams }) .then(({ items, totalCount: total }) => { setRows(items); setTotalCount(total); }) .catch((e: Error) => setErr(e.message || 'Failed to load')) .finally(() => setLoading(false)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [fetchFn, debouncedSearch, page, pageSize, extraKey]); useEffect(() => { reload(); }, [reload]); return useMemo( () => ({ rows, search, setSearch, page, setPage, pageSize, setPageSize, totalCount, loading, err, reload, }), [rows, search, page, pageSize, totalCount, loading, err, reload], ); }