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