feat(hooks): consolidate data-fetching patterns with useListData and usePolledData
- 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>
This commit is contained in:
@@ -7,14 +7,11 @@
|
||||
* Re-fetches automatically when `timeRange` or `origin` changes.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { fetchBansByCountry } from "../api/map";
|
||||
import { handleFetchError } from "../utils/fetchError";
|
||||
import type { DashboardBanItem, BanOriginFilter, TimeRange } from "../types/ban";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Return type
|
||||
// ---------------------------------------------------------------------------
|
||||
import { useListData } from "./useListData";
|
||||
import type { BanOriginFilter, TimeRange } from "../types/ban";
|
||||
import type { BansByCountryResponse, MapBanItem } from "../types/map";
|
||||
|
||||
/** Return value shape for {@link useDashboardCountryData}. */
|
||||
export interface UseDashboardCountryDataResult {
|
||||
@@ -23,7 +20,7 @@ export interface UseDashboardCountryDataResult {
|
||||
/** ISO alpha-2 country code → human-readable country name. */
|
||||
countryNames: Record<string, string>;
|
||||
/** All ban records in the selected window. */
|
||||
bans: DashboardBanItem[];
|
||||
bans: MapBanItem[];
|
||||
/** Total ban count in the window. */
|
||||
total: number;
|
||||
/** True while a fetch is in flight. */
|
||||
@@ -34,10 +31,6 @@ export interface UseDashboardCountryDataResult {
|
||||
reload: () => void;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Fetch and expose ban-by-country data for dashboard charts.
|
||||
*
|
||||
@@ -52,47 +45,28 @@ export function useDashboardCountryData(
|
||||
): UseDashboardCountryDataResult {
|
||||
const [countries, setCountries] = useState<Record<string, number>>({});
|
||||
const [countryNames, setCountryNames] = useState<Record<string, string>>({});
|
||||
const [bans, setBans] = useState<DashboardBanItem[]>([]);
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const fetcher = useCallback(
|
||||
(signal: AbortSignal) =>
|
||||
fetchBansByCountry(timeRange, origin, source, undefined, signal),
|
||||
[timeRange, origin, source],
|
||||
);
|
||||
|
||||
const load = useCallback((): void => {
|
||||
// Abort any in-flight request.
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
const selector = useCallback((response: BansByCountryResponse) => response.bans, []);
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const onSuccess = useCallback((response: BansByCountryResponse) => {
|
||||
setCountries(response.countries);
|
||||
setCountryNames(response.country_names);
|
||||
setTotal(response.total);
|
||||
}, []);
|
||||
|
||||
fetchBansByCountry(timeRange, origin, source, undefined, controller.signal)
|
||||
.then((data) => {
|
||||
if (controller.signal.aborted) return;
|
||||
setCountries(data.countries);
|
||||
setCountryNames(data.country_names);
|
||||
setBans(data.bans);
|
||||
setTotal(data.total);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (controller.signal.aborted) return;
|
||||
handleFetchError(err, setError, "Failed to fetch dashboard country data");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!controller.signal.aborted) {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}, [timeRange, origin, source]);
|
||||
const { items: bans, loading, error, refresh } = useListData<BansByCountryResponse, MapBanItem>({
|
||||
fetcher,
|
||||
selector,
|
||||
errorMessage: "Failed to fetch dashboard country data",
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
return (): void => {
|
||||
abortRef.current?.abort();
|
||||
};
|
||||
}, [load]);
|
||||
|
||||
return { countries, countryNames, bans, total, loading, error, reload: load };
|
||||
return { countries, countryNames, bans, total, loading, error, reload: refresh };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user