Files
jobtrackingapp/job-tracker-ui/src/hooks/useViewResource.ts
T

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]);
}