fix(#16): Establish consistent API usage layering patterns
- Refactor useActiveBans to use useListData generic hook instead of inline state management - Refactor useBans to use useListData generic hook for consistency - Add comprehensive 'API Usage Layering' section to Web-Development.md documenting: - Tier 1: API Functions (pure wrappers around HTTP calls) - Tier 2: Reusable Generic Hooks (useListData, useConfigItem for common patterns) - Tier 3: Domain Hooks (compose Tier 2 with domain-specific logic) - Tier 4: Components (receive data/actions via props or context) - Document pattern for action callbacks with automatic data refresh - List anti-patterns to avoid for future consistency These changes improve composability, testability, and reduce code duplication by establishing a clear convention for data-fetching patterns across the frontend. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -187,6 +187,65 @@ export function useSharedSetupStatus(): UseSharedSetupStatusResult {
|
||||
- Cache TTL should be relatively short (30 seconds) unless the data is truly static
|
||||
- Subscribers receive notifications when the cache is invalidated, allowing them to trigger a fresh fetch if needed
|
||||
|
||||
### API Usage Layering
|
||||
|
||||
Data fetching in BanGUI follows a strict, composable three-tier pattern to ensure consistency and testability:
|
||||
|
||||
**Tier 1: API Functions** (`api/*.ts`)
|
||||
- Pure, typed wrappers around HTTP calls to backend endpoints
|
||||
- Accept an optional `signal?: AbortSignal` for request cancellation (GET only)
|
||||
- Never manage state, handle errors, or retry logic
|
||||
- Example: `fetchBans(range, page, pageSize, origin, source, signal)` returns typed data
|
||||
|
||||
**Tier 2: Reusable Generic Hooks** (`hooks/useListData.ts`, `hooks/useConfigItem.ts`)
|
||||
- Provide common state management patterns: fetch + abort + error handling + refresh
|
||||
- Accept a `fetcher` function (Tier 1 API function) and a `selector` (to extract data from response)
|
||||
- Return structured results: `{ data, loading, error, refresh }`
|
||||
- Use for: lists, single-item configs, paginated data — any fetch-and-display pattern
|
||||
- Automatically abort in-flight requests on unmount
|
||||
- Example: `useListData({ fetcher: (signal) => fetchBans(..., signal), selector: (res) => res.items })`
|
||||
|
||||
**Tier 3: Domain Hooks** (`hooks/useBans.ts`, `hooks/useActiveBans.ts`, etc.)
|
||||
- Compose Tier 2 generic hooks and add domain-specific actions
|
||||
- Manage domain state (e.g., `page`, `total`), expose action callbacks (`banIp`, `unbanIp`)
|
||||
- Return a domain-specific result shape (e.g., `{ banItems, total, page, setPage, banIp, unbanIp, ... }`)
|
||||
- Called by pages to feed state to components or context providers
|
||||
- Example:
|
||||
```ts
|
||||
const fetcher = useCallback((signal) => fetchBans(timeRange, page, ..., signal), [timeRange, page]);
|
||||
const { items: banItems, ... } = useListData({ fetcher, selector: (res) => res.items });
|
||||
return { banItems, total, banIp: doBan, ... };
|
||||
```
|
||||
|
||||
**Tier 4: Components** (`components/*.tsx`, `pages/*.tsx`)
|
||||
- Never call API functions or Tier 1 functions directly
|
||||
- Receive data and actions via **props or context** — never create hooks themselves
|
||||
- Emit changes via callbacks: `onClick={() => props.onBan(jail, ip)}`
|
||||
- Remain presentational and fully testable without backend mocks
|
||||
|
||||
**Pattern for action callbacks with data refresh:**
|
||||
When a component action needs to update the displayed list, have the domain hook refresh automatically:
|
||||
```ts
|
||||
const doBan = useCallback(
|
||||
async (jail: string, ip: string): Promise<void> => {
|
||||
await banIp(jail, ip); // Tier 1 API call
|
||||
refresh(); // Re-fetch the list from Tier 2
|
||||
},
|
||||
[refresh],
|
||||
);
|
||||
```
|
||||
|
||||
**When to use Tier 2 vs Tier 3:**
|
||||
- Use `useListData` / `useConfigItem` for any new data-fetching scenario that matches the pattern
|
||||
- Only write Tier 3 hooks when you need domain-specific logic (actions, computed values, complex state)
|
||||
- Avoid duplicating Tier 3 hooks for identical patterns — refactor to a shared Tier 2 generic instead
|
||||
|
||||
**Anti-patterns to avoid:**
|
||||
- ❌ Components calling `fetchBans()` directly — violates Tier 4 rule
|
||||
- ❌ Tier 3 hooks managing all state inline instead of using Tier 2 generics — reduces reusability
|
||||
- ❌ 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
|
||||
|
||||
---
|
||||
|
||||
## 4. Code Organization
|
||||
|
||||
Reference in New Issue
Block a user