/** * Generic hook for loading and polling single-item data from an API endpoint. * * Composes useFetchData and adds polling and window-focus refetch semantics * for non-list endpoints that need periodic updates. */ import { useEffect, useRef } from "react"; import { useFetchData } from "./useFetchData"; import type { FetchError } from "../types/api"; export interface UsePolledDataOptions { /** Async function that accepts an AbortSignal for cancellation. */ fetcher: (signal: AbortSignal) => Promise; /** Synchronous selector to extract data from response. */ selector: (response: TResponse) => TData; /** Human-readable error message used as fallback if fetch fails. */ errorMessage: string; /** Optional callback invoked after successful fetch with full response. */ onSuccess?: (response: TResponse) => void; /** Initial data value. Defaults to null. */ initialData?: TData; /** Polling interval in milliseconds. If unset, no periodic polling. */ pollInterval?: number; /** If true, automatically refetch when browser window regains focus. Defaults to true. */ refetchOnWindowFocus?: boolean; } export interface UsePolledDataResult { /** The extracted data from the most recent successful fetch, or null if not yet fetched. */ data: TData | null; /** True while a fetch is in-flight, false when complete. */ loading: boolean; /** Typed error or null. Check `error?.type` to handle specific failure modes. */ error: FetchError | null; /** Trigger a fresh fetch. Cancels any in-flight request first. */ refresh: () => void; } /** * Load a single-item response and expose refresh semantics with polling support. * * Provides typed error handling through the `error` property, which is either * `null` or a discriminated `FetchError` union. Use the error's `type` field * to determine how to handle it: * * - `"api_error"`: Server returned HTTP error (check `status` for 401/403/50x) * - `"network_error"`: Network, DNS, or JSON parse failure * - `"abort_error"`: Request was cancelled (typically silently ignored by hook) * * @param options - Configuration options * @returns Data, loading state, typed error, and refresh callback */ export function usePolledData( options: UsePolledDataOptions, ): UsePolledDataResult { const { fetcher, selector, errorMessage, onSuccess, initialData, pollInterval, refetchOnWindowFocus = true, } = options; const { data, loading, error, refresh } = useFetchData({ fetcher, selector, errorMessage, onSuccess, initialData, }); const refreshRef = useRef(refresh); useEffect(() => { refreshRef.current = refresh; }, [refresh]); // Polling effect: set up interval if pollInterval is provided useEffect(() => { if (!pollInterval) { return; } const id = setInterval((): void => { refreshRef.current(); }, pollInterval); return (): void => { clearInterval(id); }; }, [pollInterval]); // Window focus: optional refetch on regain focus useEffect(() => { if (!refetchOnWindowFocus) return; const onFocus = (): void => { refreshRef.current(); }; window.addEventListener("focus", onFocus); return (): void => { window.removeEventListener("focus", onFocus); }; }, [refetchOnWindowFocus]); return { data: data ?? null, loading, error, refresh }; }