import { useCallback, useEffect, useReducer } from "react"; type FetchTransition<P> = | { type: "fetch"; } | { type: "fetchOK"; data: P; } | { type: "fetchError"; error: Error; }; type FetchState<P> = | DryState | DryErrorState | FetchingState | RefetchingState<P> | FreshState<P> | StaleState<P>; interface DryState { data: null; error: null; isLoading: false; type: "dry"; } interface DryErrorState { data: null; error: Error; isLoading: false; type: "dryError"; } interface FetchingState { data: null; error: null; isLoading: true; type: "fetching"; } interface RefetchingState<P> { data: P; error: null; isLoading: true; type: "refetching"; } interface FreshState<P> { data: P; error: null; isLoading: false; type: "fresh"; } interface StaleState<P> { data: P; error: Error; isLoading: false; type: "stale"; } export const stateFactory = { dry(): DryState { return { data: null, error: null, isLoading: false, type: "dry", }; }, dryError(error: Error): DryErrorState { return { data: null, error, isLoading: false, type: "dryError", }; }, fetching(): FetchingState { return { data: null, error: null, isLoading: true, type: "fetching", }; }, fresh<P>(data: P): FreshState<P> { return { data, error: null, isLoading: false, type: "fresh", }; }, refetching<P>(cachedData: P): RefetchingState<P> { return { data: cachedData, error: null, isLoading: true, type: "refetching", }; }, stale<P>(error: Error, cachedData: P): StaleState<P> { return { data: cachedData, error, isLoading: false, type: "stale", }; }, }; export function fetchReducer<P>( state: FetchState<P>, transition: FetchTransition<P> ): FetchState<P> { if (state.type === "dry" && transition.type === "fetch") { return stateFactory.fetching(); } if (state.type === "dryError" && transition.type === "fetch") { return stateFactory.fetching(); } if (state.type === "fetching" && transition.type === "fetchOK") { return stateFactory.fresh(transition.data); } if (state.type === "fetching" && transition.type === "fetchError") { return stateFactory.dryError(transition.error); } if (state.type === "fresh" && transition.type === "fetch") { return stateFactory.refetching(state.data); } if (state.type === "refetching" && transition.type === "fetchOK") { return stateFactory.fresh(transition.data); } if (state.type === "refetching" && transition.type === "fetchError") { return stateFactory.stale(transition.error, state.data); } if (state.type === "stale" && transition.type === "fetch") { return stateFactory.refetching(state.data); } console.warn( `Unauthorized transition ${transition.type} while in state ${state.type}` ); return state; } export default function useFetch<P>({ url, autoFetch, }: { url: string; autoFetch: boolean; }) { const [state, dispatch] = useReducer(fetchReducer, void 0, stateFactory.dry); const launchFetch = useCallback(() => { if (!url) return; dispatch({ type: "fetch" }); }, [url]); useEffect( function runFetch() { let canceled = false; if (state.isLoading) { (async () => { try { const response = await fetch(url); if (canceled) return; if (!response.ok) { throw new Error(response.statusText); } const data = await response.json(); if (canceled) return; dispatch({ type: "fetchOK", data }); } catch (error) { if (canceled) return; dispatch({ type: "fetchError", error }); } })(); } return () => { canceled = true; }; }, [url, state.isLoading] ); useEffect(() => { if (autoFetch) { launchFetch(); } }, [autoFetch, launchFetch]); return { state: state as FetchState<P>, fetch }; }