fix(frontend): deduplicate setup status API calls using shared hook
Implement request deduplication to prevent multiple duplicate calls to GET /api/setup when multiple components mount simultaneously. The fix introduces: 1. New 'useSharedSetupStatus' hook with module-level caching - Shares a single in-flight request across all consumers - Implements 30-second cache TTL with cache invalidation - Notifies all subscribers when cache is invalidated 2. Refactored 'useSetup' hook to use shared cache - Internally uses useSharedSetupStatus for status checks - Calls invalidateSetupStatus() after successful setup submission - Maintains backward-compatible API 3. Updated components using setup status - SetupGuard and SetupPage automatically benefit from deduplication - No changes needed to consumer code 4. Updated tests - Mocked useSharedSetupStatus in component tests - Added comprehensive tests for cache behavior - All existing tests pass 5. Documentation updates - Added 'Request Deduplication & Shared Caching' section to Web-Development.md - Explains when and how to use shared hooks - Provides complete implementation example This eliminates wasted resources from duplicate API calls and potential race conditions where different requests return slightly different states. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -114,6 +114,66 @@ fetchBans(24, ctrl.signal) // Pass the signal to enable cancellation on unmount
|
||||
.catch(err => { /* ... */ });
|
||||
```
|
||||
|
||||
### Request Deduplication & Shared Caching
|
||||
|
||||
When multiple components mount simultaneously and need the same data, **implement shared hooks with request deduplication** to avoid duplicate API calls. Use a module-level cache to ensure all consumers share a single in-flight request:
|
||||
|
||||
- Create a custom hook with module-level state to track in-flight requests
|
||||
- When multiple hook instances request the same data concurrently, they await the same promise
|
||||
- Implement cache invalidation via an exported function that notifies all subscribers
|
||||
- Consumers call the shared hook instead of raw API functions
|
||||
|
||||
```ts
|
||||
// hooks/useSharedSetupStatus.ts — shared, deduplicated setup status
|
||||
const subscribers: Set<() => void> = new Set();
|
||||
let cache: CacheEntry | null = null;
|
||||
|
||||
export function invalidateSetupStatus(): void {
|
||||
cache = null;
|
||||
subscribers.forEach(notify => notify());
|
||||
}
|
||||
|
||||
export function useSharedSetupStatus(): UseSharedSetupStatusResult {
|
||||
const [status, setStatus] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const now = Date.now();
|
||||
const isCacheValid = cache && now - cache.timestamp < 30000;
|
||||
|
||||
if (!isCacheValid) {
|
||||
cache = {
|
||||
promise: getSetupStatus(),
|
||||
timestamp: now,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await cache.promise;
|
||||
setStatus(result);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
subscribers.add(refresh);
|
||||
return () => { subscribers.delete(refresh); };
|
||||
}, [refresh]);
|
||||
|
||||
return { status, loading, error, refresh };
|
||||
}
|
||||
```
|
||||
|
||||
**When to use shared hooks:**
|
||||
- When a critical status or configuration is checked by multiple components on mount (e.g., setup completion, session validation, feature flags)
|
||||
- When concurrent requests for the same data waste backend resources or introduce race conditions
|
||||
- When cache TTL is short and invalidation is simple
|
||||
|
||||
**Guidelines:**
|
||||
- Shared hooks should be used in low-level consumer code (direct consumers of the setup flow)
|
||||
- The cache can be **invalidated explicitly** after mutations (e.g., after setup completes, call `invalidateSetupStatus()`)
|
||||
- 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
|
||||
|
||||
---
|
||||
|
||||
## 4. Code Organization
|
||||
|
||||
Reference in New Issue
Block a user