Files
BanGUI/frontend/src/hooks/useSetup.ts
Lukas 3095fa3313 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>
2026-04-26 13:32:44 +02:00

72 lines
2.1 KiB
TypeScript

/**
* 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, useState } from "react";
import { ApiError } from "../api/client";
import { submitSetup } from "../api/setup";
import { invalidateSetupStatus, useSharedSetupStatus } from "./useSharedSetupStatus";
import type {
SetupRequest,
SetupStatusResponse,
} from "../types/setup";
export interface UseSetupResult {
/** Known setup status, or null while loading. */
status: SetupStatusResponse | null;
/** Whether the initial status check is in progress. */
loading: boolean;
/** User-facing error message from the last status check. */
error: string | null;
/** Refresh the setup status from the backend. */
refresh: () => Promise<void>;
/** Whether a submit request is currently in flight. */
submitting: boolean;
/** User-facing error message from the last submit attempt. */
submitError: string | null;
/** Submit the initial setup payload. */
submit: (payload: SetupRequest) => Promise<void>;
}
export function useSetup(): UseSetupResult {
const { status, loading, error, refresh } = useSharedSetupStatus();
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const submit = useCallback(async (payload: SetupRequest): Promise<void> => {
setSubmitting(true);
setSubmitError(null);
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);
} else if (err instanceof Error) {
setSubmitError(err.message);
} else {
setSubmitError("An unexpected error occurred.");
}
throw err;
} finally {
setSubmitting(false);
}
}, []);
return {
status,
loading,
error,
refresh,
submitting,
submitError,
submit,
};
}