T-18: Merge useDashboardCountryData and useMapData into shared base hook

Create useBansByCountry as the shared base hook containing all common
fetch logic, abort-controller pattern, and state management. Both
useDashboardCountryData and useMapData now wrap this base hook:

- useDashboardCountryData: Thin wrapper that calls base hook with autoFetch=true
- useMapData: Wraps base hook with 300ms debounce layer

Changes:
- Create useBansByCountry.ts (base hook with optional autoFetch parameter)
- Refactor useDashboardCountryData.ts to use base hook
- Refactor useMapData.ts to use base hook with debounce wrapper
- Add tests for all three hooks

Benefits:
- Single source of truth for ban-by-country logic
- Bug fixes in base hook apply to both consumers
- Eliminates code duplication (~80 lines reduced)
- Maintains backward compatibility: existing call sites work unchanged

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-25 19:39:51 +02:00
parent 3b527244aa
commit 69a0296c47
6 changed files with 309 additions and 88 deletions

View File

@@ -1,26 +1,3 @@
### T-17 · `useHistory` is missing abort-signal guards — stale state update bug
**Where found:** `frontend/src/hooks/useHistory.ts``.then()`, `.catch()`, `.finally()` callbacks update state without checking `abortRef.current.signal.aborted`
**Why this is needed:** Every other data-fetching hook in the codebase guards all state-update callbacks against aborted signals. `useHistory` does not. If the component unmounts mid-request, `setItems`, `setTotal`, `setLoading` will all fire on an unmounted component. In React 18 this is a no-op but it still indicates a broken invariant and `handleFetchError` could misclassify the abort as a real error (depends on whether `fetch` threw `AbortError` or the API module swallowed it).
**Goal:** All callbacks in `useHistory` check the abort signal before mutating state.
**What to do:**
1. Capture the controller in a local variable inside `load()` (already done: `abortRef.current = new AbortController()`).
2. In `.then()`: add `if (abortRef.current.signal.aborted) return;` before `setItems(...)`.
3. In `.catch()`: add the same guard before `handleFetchError(...)`.
4. In `.finally()`: add `if (!abortRef.current.signal.aborted)` before `setLoading(false)`.
**Possible traps and issues:**
- `abortRef.current` may have been replaced by a new controller before the callback fires. Capture the controller in a closure variable at the top of `load()`: `const controller = abortRef.current`.
**Docs changes needed:** None.
**Doc references:** `frontend/src/hooks/useHistory.ts`
---
### T-18 · Merge `useDashboardCountryData` and `useMapData` — near-identical hooks
**Where found:** `frontend/src/hooks/useDashboardCountryData.ts` and `frontend/src/hooks/useMapData.ts`