Files
BanGUI/frontend/src/hooks/useListData.ts

71 lines
1.9 KiB
TypeScript

/**
* 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<TResponse, TItem> {
fetcher: (signal: AbortSignal) => Promise<TResponse>;
selector: (response: TResponse) => TItem[];
errorMessage: string;
onSuccess?: (response: TResponse) => void;
initialItems?: TItem[];
}
export interface UseListDataResult<TItem> {
items: TItem[];
loading: boolean;
error: string | null;
refresh: () => void;
}
/**
* Load a list response and expose refresh semantics with abort support.
*/
export function useListData<TResponse, TItem>(
options: UseListDataOptions<TResponse, TItem>,
): UseListDataResult<TItem> {
const { fetcher, selector, errorMessage, onSuccess, initialItems } = options;
const [items, setItems] = useState<TItem[]>(initialItems ?? []);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(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 };
}