Skip to content

Instantly share code, notes, and snippets.

@melpon
Last active September 10, 2019 16:25
Show Gist options
  • Save melpon/acfc9845a855b439bc374cbd43821d36 to your computer and use it in GitHub Desktop.
Save melpon/acfc9845a855b439bc374cbd43821d36 to your computer and use it in GitHub Desktop.
import React from "react";
export type AnyJson = boolean | number | string | null | JsonArray | JsonMap;
export interface JsonMap {
[key: string]: AnyJson;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface JsonArray extends Array<AnyJson> {}
type Resolver<T> = (resp: AnyJson | string | Blob | FormData) => T;
export function useFetch<T>(
mode: "json" | "text" | "blob" | "formData",
defaultUrl: string | null,
defaultOpts: RequestInit,
resolver: Resolver<T>,
onError: (error: string) => void
): [T | null, number, ((url: string | null, opts: RequestInit) => void)] {
const [response, setResponse] = React.useState<T | null>(null);
const [counter, setCounter] = React.useState<number>(0);
const [fetchCounter, setFetchCounter] = React.useState<number>(0);
const [url, setUrl] = React.useState<string | null>(null);
const [opts, setOpts] = React.useState<RequestInit>({});
const didMountRef = React.useRef(false);
React.useEffect((): (() => void) | undefined => {
// 初回は実行しない
if (!didMountRef.current) {
didMountRef.current = true;
return;
}
const abortController = new AbortController();
(async (): Promise<void> => {
if (url === null) {
onError("URL が指定されていません");
return;
}
try {
const payload = await fetch(url, {
...opts,
signal: abortController.signal
});
if (Math.floor(payload.status / 100) !== 2) {
onError(
`${url} リクエストエラー: ステータスコード=${payload.status}`
);
return;
}
if (mode === "json") {
setResponse(resolver((await payload.json()) as AnyJson));
} else if (mode === "text") {
setResponse(resolver(await payload.text()));
} else if (mode === "blob") {
setResponse(resolver(await payload.blob()));
} else if (mode === "formData") {
setResponse(resolver(await payload.formData()));
}
setFetchCounter((counter): number => counter + 1);
} catch (error) {
onError(`${url} リクエストエラー: エラー=${String(error)}`);
}
})();
const cleanup = (): void => {
abortController.abort();
};
return cleanup;
}, [counter]);
const doFetch = React.useCallback(
(url, opts): void => {
setUrl(url || defaultUrl);
setOpts(Object.assign(defaultOpts, opts));
setCounter((counter): number => counter + 1);
},
[defaultUrl, defaultOpts]
);
return [response, fetchCounter, doFetch];
}
export function useFetchJSON<T>(
url: string | null,
opts: RequestInit,
resolver: (resp: AnyJson) => T,
onError: (error: string) => void
): [T | null, number, ((url: string | null, opts: RequestInit) => void)] {
return useFetch<T>("json", url, opts, resolver as Resolver<T>, onError);
}
export function useFetchText<T>(
url: string | null,
opts: RequestInit,
resolver: (resp: string) => T,
onError: (error: string) => void
): [T | null, number, ((url: string | null, opts: RequestInit) => void)] {
return useFetch<T>("text", url, opts, resolver as Resolver<T>, onError);
}
export function useFetchBlob<T>(
url: string | null,
opts: RequestInit,
resolver: (resp: Blob) => T,
onError: (error: string) => void
): [T | null, number, ((url: string | null, opts: RequestInit) => void)] {
return useFetch("blob", url, opts, resolver as Resolver<T>, onError);
}
export function useFetchFormData<T>(
url: string | null,
opts: RequestInit,
resolver: (resp: FormData) => T,
onError: (error: string) => void
): [T | null, number, ((url: string | null, opts: RequestInit) => void)] {
return useFetch("formData", url, opts, resolver as Resolver<T>, onError);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment