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:
@@ -2,12 +2,14 @@
|
||||
* Hook for the initial BanGUI setup flow.
|
||||
*
|
||||
* Exposes the current setup completion status and a submission handler.
|
||||
* Uses the shared setup status hook to deduplicate requests when multiple
|
||||
* consumers mount simultaneously.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { ApiError } from "../api/client";
|
||||
import { handleFetchError } from "../utils/fetchError";
|
||||
import { getSetupStatus, submitSetup } from "../api/setup";
|
||||
import { submitSetup } from "../api/setup";
|
||||
import { invalidateSetupStatus, useSharedSetupStatus } from "./useSharedSetupStatus";
|
||||
import type {
|
||||
SetupRequest,
|
||||
SetupStatusResponse,
|
||||
@@ -31,48 +33,9 @@ export interface UseSetupResult {
|
||||
}
|
||||
|
||||
export function useSetup(): UseSetupResult {
|
||||
const [status, setStatus] = useState<SetupStatusResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { status, loading, error, refresh } = useSharedSetupStatus();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const refresh = useCallback(async (): Promise<void> => {
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
const { signal } = controller;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const resp = await getSetupStatus(signal);
|
||||
if (signal.aborted) {
|
||||
return;
|
||||
}
|
||||
setStatus(resp);
|
||||
} catch (err: unknown) {
|
||||
if (signal.aborted) {
|
||||
return;
|
||||
}
|
||||
const fallback = "Failed to fetch setup status";
|
||||
handleFetchError(err, setError, fallback);
|
||||
} finally {
|
||||
if (!signal.aborted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
|
||||
return (): void => {
|
||||
abortRef.current?.abort();
|
||||
};
|
||||
}, [refresh]);
|
||||
|
||||
const submit = useCallback(async (payload: SetupRequest): Promise<void> => {
|
||||
setSubmitting(true);
|
||||
@@ -80,6 +43,8 @@ export function useSetup(): UseSetupResult {
|
||||
|
||||
try {
|
||||
await submitSetup(payload);
|
||||
// Invalidate the cache after successful setup so the next check reflects the new state.
|
||||
invalidateSetupStatus();
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
setSubmitError(err.message);
|
||||
|
||||
Reference in New Issue
Block a user