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:
2026-04-25 19:08:26 +02:00
parent b44b72053a
commit 8d30a81346
7 changed files with 244 additions and 281 deletions

View File

@@ -2,7 +2,7 @@
* React hook for loading and controlling the jail overview list.
*/
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useState } from "react";
import {
fetchJails,
reloadAllJails,
@@ -11,8 +11,8 @@ import {
startJail,
stopJail,
} from "../api/jails";
import { handleFetchError } from "../utils/fetchError";
import type { JailSummary } from "../types/jail";
import { useListData } from "./useListData";
import type { JailSummary, JailListResponse } from "../types/jail";
export interface UseJailsResult {
jails: JailSummary[];
@@ -31,83 +31,64 @@ export interface UseJailsResult {
* Fetch and manage the jail overview list.
*/
export function useJails(): UseJailsResult {
const [jails, setJails] = useState<JailSummary[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(null);
const load = useCallback(() => {
abortRef.current?.abort();
const ctrl = new AbortController();
abortRef.current = ctrl;
setLoading(true);
setError(null);
const fetcher = useCallback(
(signal: AbortSignal) => fetchJails(signal),
[],
);
fetchJails(ctrl.signal)
.then((res) => {
if (!ctrl.signal.aborted) {
setJails(res.jails);
setTotal(res.total);
}
})
.catch((err: unknown) => {
if (!ctrl.signal.aborted) {
handleFetchError(err, setError, "Failed to load jails");
}
})
.finally(() => {
if (!ctrl.signal.aborted) {
setLoading(false);
}
});
const selector = useCallback((response: JailListResponse) => response.jails, []);
const onSuccess = useCallback((response: JailListResponse) => {
setTotal(response.total);
}, []);
useEffect(() => {
load();
return (): void => {
abortRef.current?.abort();
};
}, [load]);
const { items: jails, loading, error, refresh } = useListData<JailListResponse, JailSummary>({
fetcher,
selector,
errorMessage: "Failed to load jails",
onSuccess,
});
const startJailMemo = useCallback(
async (name: string): Promise<void> => {
await startJail(name);
load();
refresh();
},
[load],
[refresh],
);
const stopJailMemo = useCallback(
async (name: string): Promise<void> => {
await stopJail(name);
load();
refresh();
},
[load],
[refresh],
);
const reloadJailMemo = useCallback(
async (name: string): Promise<void> => {
await reloadJail(name);
load();
refresh();
},
[load],
[refresh],
);
const setIdleMemo = useCallback(
(name: string, on: boolean): Promise<void> =>
setJailIdle(name, on).then(() => {
load();
refresh();
}),
[load],
[refresh],
);
const reloadAllMemo = useCallback(
(): Promise<void> =>
reloadAllJails().then(() => {
load();
refresh();
}),
[load],
[refresh],
);
return {
@@ -115,7 +96,7 @@ export function useJails(): UseJailsResult {
total,
loading,
error,
refresh: load,
refresh,
startJail: startJailMemo,
stopJail: stopJailMemo,
setIdle: setIdleMemo,