- Refactor useJails (useJailList.ts) to use useListData with onSuccess for total - Refactor useBanTrend to use useListData with onSuccess for bucket_size - Refactor useDashboardCountryData to use useListData with onSuccess for aggregated data - Refactor useHistory to use useListData with proper abort guard in finally() - Create usePolledData for single-item endpoints with polling and window focus refetch - Refactor useServerStatus to use usePolledData for 30s polling + window focus refetch - Keep useIpHistory with manual pattern (single-item, no list semantics) - Document deferred refactoring of useJailDetail (depends on T-13 for data/command split) All data-fetching hooks now follow one of two consistent patterns: 1. useListData: for paginated/list endpoints with refresh semantics 2. usePolledData: for single-item endpoints with polling and focus-refetch This eliminates code duplication, centralizes abort-guard logic, and enables consistent fixes across all data-fetching hooks. Resolves T-12. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
116 lines
3.0 KiB
TypeScript
116 lines
3.0 KiB
TypeScript
/**
|
|
* Generic hook for loading and polling single-item data from an API endpoint.
|
|
*
|
|
* Similar to useListData, but for non-list endpoints that need periodic polling
|
|
* and window-focus refetch semantics.
|
|
*/
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { handleFetchError } from "../utils/fetchError";
|
|
|
|
export interface UsePolledDataOptions<TResponse, TData> {
|
|
fetcher: (signal: AbortSignal) => Promise<TResponse>;
|
|
selector: (response: TResponse) => TData;
|
|
errorMessage: string;
|
|
onSuccess?: (response: TResponse) => void;
|
|
initialData?: TData;
|
|
pollInterval?: number;
|
|
refetchOnWindowFocus?: boolean;
|
|
}
|
|
|
|
export interface UsePolledDataResult<TData> {
|
|
data: TData | null;
|
|
loading: boolean;
|
|
error: string | null;
|
|
refresh: () => void;
|
|
}
|
|
|
|
/**
|
|
* Load a single-item response and expose refresh semantics with polling support.
|
|
*
|
|
* @param options - Configuration options
|
|
* @returns Data, loading state, error, and refresh callback
|
|
*/
|
|
export function usePolledData<TResponse, TData>(
|
|
options: UsePolledDataOptions<TResponse, TData>,
|
|
): UsePolledDataResult<TData> {
|
|
const {
|
|
fetcher,
|
|
selector,
|
|
errorMessage,
|
|
onSuccess,
|
|
initialData,
|
|
pollInterval,
|
|
refetchOnWindowFocus = true,
|
|
} = options;
|
|
|
|
const [data, setData] = useState<TData | null>(initialData ?? null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const abortRef = useRef<AbortController | null>(null);
|
|
const fetchRef = useRef<() => void>((): void => undefined);
|
|
|
|
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;
|
|
setData(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]);
|
|
|
|
fetchRef.current = refresh;
|
|
|
|
useEffect(() => {
|
|
refresh();
|
|
|
|
if (!pollInterval) {
|
|
return (): void => {
|
|
abortRef.current?.abort();
|
|
};
|
|
}
|
|
|
|
const id = setInterval((): void => {
|
|
fetchRef.current();
|
|
}, pollInterval);
|
|
|
|
return (): void => {
|
|
clearInterval(id);
|
|
abortRef.current?.abort();
|
|
};
|
|
}, [refresh, pollInterval]);
|
|
|
|
// Refetch on window focus if enabled.
|
|
useEffect(() => {
|
|
if (!refetchOnWindowFocus) return;
|
|
|
|
const onFocus = (): void => {
|
|
fetchRef.current();
|
|
};
|
|
|
|
window.addEventListener("focus", onFocus);
|
|
return (): void => {
|
|
window.removeEventListener("focus", onFocus);
|
|
};
|
|
}, [refetchOnWindowFocus]);
|
|
|
|
return { data, loading, error, refresh };
|
|
}
|