100 lines
2.6 KiB
TypeScript
100 lines
2.6 KiB
TypeScript
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<T> = {
|
|
data: T;
|
|
loading: boolean;
|
|
refreshing: boolean;
|
|
error: ViewResourceError | null;
|
|
hasLoaded: boolean;
|
|
reload: () => Promise<void>;
|
|
setData: Dispatch<SetStateAction<T>>;
|
|
};
|
|
|
|
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<T>(
|
|
load: () => Promise<T>,
|
|
options: {
|
|
initialData: T;
|
|
errorMessage: string;
|
|
deps?: DependencyList;
|
|
enabled?: boolean;
|
|
},
|
|
): ViewResourceState<T> {
|
|
const { initialData, errorMessage, deps = [], enabled = true } = options;
|
|
const [data, setData] = useState<T>(initialData);
|
|
const [loading, setLoading] = useState(enabled);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [hasLoaded, setHasLoaded] = useState(false);
|
|
const [error, setError] = useState<ViewResourceError | null>(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]);
|
|
}
|