import { DependencyList, Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; import { getApiErrorMessage } from "../api"; export type ViewResourceErrorKind = "unauthorized" | "unavailable" | "error"; export type ViewResourceError = { kind: ViewResourceErrorKind; message: string; retryable: boolean; status?: number; }; export type ViewResourceState = { data: T; loading: boolean; refreshing: boolean; error: ViewResourceError | null; hasLoaded: boolean; reload: () => Promise; setData: Dispatch>; }; function normalizeError(error: any, fallback: string): ViewResourceError { const status = error?.response?.status as number | undefined; if (status === 401 || status === 403) { return { kind: "unauthorized", message: getApiErrorMessage(error, fallback), retryable: false, status, }; } if (!status || status >= 500) { return { kind: "unavailable", message: getApiErrorMessage(error, fallback), retryable: true, status, }; } return { kind: "error", message: getApiErrorMessage(error, fallback), retryable: true, status, }; } export function useViewResource( load: () => Promise, options: { initialData: T; errorMessage: string; deps?: DependencyList; enabled?: boolean; }, ): ViewResourceState { const { initialData, errorMessage, deps = [], enabled = true } = options; const [data, setData] = useState(initialData); const [loading, setLoading] = useState(enabled); const [refreshing, setRefreshing] = useState(false); const [hasLoaded, setHasLoaded] = useState(false); const [error, setError] = useState(null); const reload = useCallback(async () => { if (!enabled) return; setLoading((current) => !hasLoaded && current); setRefreshing(hasLoaded); try { const next = await load(); setData(next); setError(null); setHasLoaded(true); } catch (err: any) { setError(normalizeError(err, errorMessage)); setHasLoaded(true); } finally { setLoading(false); setRefreshing(false); } }, [enabled, errorMessage, hasLoaded, load]); useEffect(() => { if (!enabled) { setLoading(false); return; } setLoading(!hasLoaded); void reload(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [enabled, reload, ...deps]); return useMemo(() => ({ data, loading, refreshing, error, hasLoaded, reload, setData }), [data, error, hasLoaded, loading, refreshing, reload]); }