/** * Generic hook for loading list data from an API endpoint. */ import { useCallback, useEffect, useRef, useState } from "react"; import { handleFetchError } from "../utils/fetchError"; export interface UseListDataOptions { fetcher: (signal: AbortSignal) => Promise; selector: (response: TResponse) => TItem[]; errorMessage: string; onSuccess?: (response: TResponse) => void; initialItems?: TItem[]; } export interface UseListDataResult { items: TItem[]; loading: boolean; error: string | null; refresh: () => void; } /** * Load a list response and expose refresh semantics with abort support. */ export function useListData( options: UseListDataOptions, ): UseListDataResult { const { fetcher, selector, errorMessage, onSuccess, initialItems } = options; const [items, setItems] = useState(initialItems ?? []); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const abortRef = useRef(null); const refresh = useCallback((): void => { abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; setLoading(true); setError(null); fetcher(controller.signal) .then((response) => { if (controller.signal.aborted) return; setItems(selector(response)); if (onSuccess) { onSuccess(response); } }) .catch((err: unknown) => { if (controller.signal.aborted) return; handleFetchError(err, setError, errorMessage); }) .finally(() => { if (!controller.signal.aborted) { setLoading(false); } }); }, [fetcher, selector, errorMessage, onSuccess]); useEffect(() => { refresh(); return (): void => { abortRef.current?.abort(); }; }, [refresh]); return { items, loading, error, refresh }; }