Skip to content

Instantly share code, notes, and snippets.

@jgdev
Created August 27, 2023 14:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jgdev/fd24fd5cd2c72b065d846fc116d4ec8d to your computer and use it in GitHub Desktop.
Save jgdev/fd24fd5cd2c72b065d846fc116d4ec8d to your computer and use it in GitHub Desktop.
React Hook to fetch API resources
import { useCallback, useEffect, useState } from "react";
type Status = "idle" | "loading" | "error" | "fetched" | "debouncing";
export type Options = {
timeout?: number;
debounceTime?: number;
};
export type FetchAttempt<T> = Partial<RequestInit> & {
lastFetched: number;
data?: T | any;
queryString?: T | any;
};
type Return<T, K> = {
error: { detail: any } | null | undefined;
data: K | null | undefined;
status: Status;
post: (params?: Partial<FetchAttempt<T>>) => void;
patch: (params?: Partial<FetchAttempt<T>>) => void;
del: (params?: Partial<FetchAttempt<T>>) => void;
get: (params?: Partial<FetchAttempt<T>>) => void;
loading: boolean;
};
export function useFetch<T = unknown, K = unknown>(
url?: string,
options?: Options
): Return<T, K> {
const [status, setStatus] = useState<Status>("idle");
const [fetchAttempt, _setFetchAttempt] = useState<
FetchAttempt<T> | undefined
>();
const [data, setData] = useState<K | null | undefined>(null);
const [error, setError] = useState<
{ detail: any; error: any } | null | undefined
>(null);
const [loading, setLoading] = useState(false);
const setFetchAttempt = (params: FetchAttempt<T>) => {
setError(null);
setData(null);
_setFetchAttempt(params);
};
useEffect(() => {
setLoading(status === "loading");
}, [status, setLoading]);
useEffect(() => {
if (!fetchAttempt || !url) return;
setStatus("debouncing");
const timeout = setTimeout(() => {
const fetchData = async () => {
const { method, data, queryString } = fetchAttempt;
let _url = url;
if (queryString) {
_url = `${_url}?${new URLSearchParams(queryString).toString()}`;
}
setStatus("loading");
const options: RequestInit = {};
if (data) {
options.body = JSON.stringify(data);
options.headers = {
...(options.headers || {}),
"Content-Type": "application/json",
};
}
try {
const res = await fetch(_url, {
method,
...options,
});
const body = await res.json();
if (!res.ok) {
setError(body);
setStatus("error");
return;
}
setStatus("fetched");
setData(body);
} catch (err) {
setStatus("error");
setError({ detail: "Failed to fetch", error: err });
} finally {
setLoading(false);
}
};
fetchData();
}, options?.debounceTime || 0);
return () => {
clearTimeout(timeout);
};
}, [fetchAttempt, url, setLoading, options?.debounceTime]);
return {
status,
error,
data,
loading,
post: (params?: Partial<FetchAttempt<T>>) => {
setFetchAttempt({
...params,
method: "POST",
lastFetched: Date.now(),
});
},
patch: () => {},
del: () => {},
get: (params?: Partial<FetchAttempt<T>>) => {
setFetchAttempt({
...params,
data: undefined,
method: "GET",
lastFetched: Date.now(),
});
},
};
}
export const useApi = <T = unknown, K = unknown>(
method: string,
options?: Options
): Return<T, K> => {
const url = import.meta.env.VITE_API_BASE_URL;
if (!url) throw new Error("Api base url not defined");
return useFetch(url + method, options);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment