refactor, security updates, cv extraction upgrades
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { api } from "../api";
|
||||
import { Company } from "../types";
|
||||
import { useViewResource, ViewResourceError } from "./useViewResource";
|
||||
|
||||
let cachedCompanies: Company[] | null = null;
|
||||
let inflight: Promise<Company[]> | null = null;
|
||||
@@ -10,7 +12,7 @@ async function fetchCompanies(): Promise<Company[]> {
|
||||
if (inflight) return inflight;
|
||||
|
||||
inflight = api
|
||||
.get<Company[]>("/companies")
|
||||
.get<Company[]>('/companies')
|
||||
.then((r) => {
|
||||
cachedCompanies = r.data;
|
||||
return r.data;
|
||||
@@ -26,25 +28,34 @@ export function invalidateCompaniesCache() {
|
||||
cachedCompanies = null;
|
||||
}
|
||||
|
||||
export function useCompanies() {
|
||||
const [companies, setCompanies] = useState<Company[]>(cachedCompanies ?? []);
|
||||
const [loading, setLoading] = useState(!cachedCompanies);
|
||||
export function useCompanies(): {
|
||||
companies: Company[];
|
||||
loading: boolean;
|
||||
refreshing: boolean;
|
||||
error: ViewResourceError | null;
|
||||
reload: () => Promise<void>;
|
||||
} {
|
||||
const [cacheBust, setCacheBust] = useState(0);
|
||||
const resource = useViewResource(fetchCompanies, {
|
||||
initialData: cachedCompanies ?? [],
|
||||
errorMessage: 'Unable to load companies right now.',
|
||||
deps: [cacheBust],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
setLoading(!cachedCompanies);
|
||||
fetchCompanies()
|
||||
.then((c) => {
|
||||
if (mounted) setCompanies(c);
|
||||
})
|
||||
.finally(() => {
|
||||
if (mounted) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
if (!resource.error) {
|
||||
cachedCompanies = resource.data;
|
||||
}
|
||||
}, [resource.data, resource.error]);
|
||||
|
||||
return { companies, loading };
|
||||
return {
|
||||
companies: resource.data,
|
||||
loading: resource.loading,
|
||||
refreshing: resource.refreshing,
|
||||
error: resource.error,
|
||||
reload: async () => {
|
||||
invalidateCompaniesCache();
|
||||
setCacheBust((value) => value + 1);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
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]);
|
||||
}
|
||||
Reference in New Issue
Block a user