refactor(frontend): extract shared fetch lifecycle into useFetchData base hook

Eliminates ~100 lines of duplicated code across useListData and usePolledData
by creating a composable base hook that handles:
- Abort controller lifecycle and cancellation
- Loading/error state management
- Fetch error handling
- Unmount cleanup

Changes:
- Create hooks/useFetchData.ts with base fetch lifecycle (no effects on consumers)
- Refactor useListData to compose useFetchData, returns items array by default
- Refactor usePolledData to compose useFetchData, adds polling and focus-refetch
- Add comprehensive tests for useFetchData base hook
- Document hook architecture and composition pattern in Web-Development.md

Result: Both hooks now use shared primitives, reducing maintenance burden
and ensuring consistent cancellation/error handling across all data fetches.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-28 09:23:34 +02:00
parent 5166789b68
commit d10145e5d6
6 changed files with 456 additions and 150 deletions

View File

@@ -246,6 +246,51 @@ const doBan = useCallback(
- ❌ Passing API functions directly to components — couples components to API contract
- ❌ Multiple domain hooks for the same data without deduplication — causes wasted requests and state desync
### Hook Architecture & Reusable Primitives
BanGUI's hooks are built on composable primitives to eliminate duplication and enforce consistent patterns.
**Base Hook: `useFetchData`** (`hooks/useFetchData.ts`)
- The foundation of all data-fetching hooks
- Encapsulates fetch lifecycle: abort controller management, loading/error state, cancellation safety
- Signature: `useFetchData(fetcher, selector, errorMessage, onSuccess?, initialData?) → { data, loading, error, refresh }`
- Not used directly by consumers; only composed by higher-level hooks
- Handles automatic cleanup on unmount (abort signal cancellation)
**Tier 2 Hooks Built on `useFetchData`:**
- `useListData`: Wraps `useFetchData` with `initialData` defaulting to `[]` and returns `{ items, ... }`
- `usePolledData`: Wraps `useFetchData` and adds polling (interval) + window-focus refetch on top
- Additional specialized hooks can be added by composing `useFetchData` with domain-specific effects
**Composition Pattern for New Hooks:**
When building a new Tier 2 hook with custom behavior, follow this pattern:
```ts
export function useMyCustomData<TResponse, TData>(options: MyOptions): MyResult {
// 1. Use useFetchData for the base fetch lifecycle
const { data, loading, error, refresh } = useFetchData({
fetcher: options.fetcher,
selector: options.selector,
errorMessage: options.errorMessage,
onSuccess: options.onSuccess,
initialData: options.initialData,
});
// 2. Add custom effects for additional behavior (e.g., polling, focus handling, custom cleanup)
useEffect(() => {
// Your custom logic here
}, [...dependencies]);
// 3. Return a domain-specific result shape
return { data, loading, error, refresh, customField: /* derived */ };
}
```
**Why this architecture?**
- **DRY**: Eliminates duplicate fetch logic across multiple hooks
- **Consistency**: All hooks share the same cancellation and error handling semantics
- **Testability**: Base hook can be tested in isolation; custom effects are minimal and easy to test
- **Maintainability**: Bug fixes to abort or error handling only need to happen once
---
## 4. Code Organization