/** * Generic hook for loading and polling single-item data from an API endpoint. * * Similar to useListData, but for non-list endpoints that need periodic polling * and window-focus refetch semantics. */ import { useCallback, useEffect, useRef, useState } from "react"; import { handleFetchError } from "../utils/fetchError"; export interface UsePolledDataOptions { fetcher: (signal: AbortSignal) => Promise; selector: (response: TResponse) => TData; errorMessage: string; onSuccess?: (response: TResponse) => void; initialData?: TData; pollInterval?: number; refetchOnWindowFocus?: boolean; } export interface UsePolledDataResult { data: TData | null; loading: boolean; error: string | null; refresh: () => void; } /** * Load a single-item response and expose refresh semantics with polling support. * * @param options - Configuration options * @returns Data, loading state, error, and refresh callback */ export function usePolledData( options: UsePolledDataOptions, ): UsePolledDataResult { const { fetcher, selector, errorMessage, onSuccess, initialData, pollInterval, refetchOnWindowFocus = true, } = options; const [data, setData] = useState(initialData ?? null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const abortRef = useRef(null); const fetchRef = useRef<() => void>((): void => undefined); const refresh = useCallback((): void => { abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; setLoading(true); setError(null); fetcher(controller.signal) .then((response) => { if (controller.signal.aborted) return; setData(selector(response)); if (onSuccess) { onSuccess(response); } }) .catch((err: unknown) => { if (controller.signal.aborted) return; handleFetchError(err, setError, errorMessage); }) .finally(() => { if (!controller.signal.aborted) { setLoading(false); } }); }, [fetcher, selector, errorMessage, onSuccess]); fetchRef.current = refresh; useEffect(() => { refresh(); if (!pollInterval) { return (): void => { abortRef.current?.abort(); }; } const id = setInterval((): void => { fetchRef.current(); }, pollInterval); return (): void => { clearInterval(id); abortRef.current?.abort(); }; }, [refresh, pollInterval]); // Refetch on window focus if enabled. useEffect(() => { if (!refetchOnWindowFocus) return; const onFocus = (): void => { fetchRef.current(); }; window.addEventListener("focus", onFocus); return (): void => { window.removeEventListener("focus", onFocus); }; }, [refetchOnWindowFocus]); return { data, loading, error, refresh }; }