import { useEffect , useState , useMemo } from 'react' ; export enum FETCH_STATUS { LOADING = 'loading' , SUCCESS = 'success' , FAILURE = 'failure' , PENDING = 'pending' , NOT_INITIATED = 'not_initiated' } export interface FetcherResult < Data > { data ? : Data ; status : FETCH_STATUS ; error ? : Error ; loading ? : boolean ; } type InferResponseType < TReturn > = Exclude < TReturn , undefined > extends Promise < infer TResponseType > ? TResponseType : unknown ; export function useFetcher < TReturn > ( fn , fnDeps , options = { } ) { const { preservePreviousData = true } = options ; const [ result , setResult ] = useState < FetcherResult < InferResponseType < TReturn > > > ( { data : undefined , status : FETCH_STATUS . PENDING , loading : true , } ) ; const [ counter , setCounter ] = useState ( 0 ) ; useEffect ( ( ) => { let controller = new AbortController ( ) ; async function doFetch ( ) { controller . abort ( ) ; controller = new AbortController ( ) ; const signal = controller . signal ; const promise = fn ( { signal } ) ; if ( ! promise ) { return ; } setResult ( ( prevResult ) => ( { data : preservePreviousData ? prevResult . data : undefined , status : FETCH_STATUS . LOADING , error : undefined , loading : true , } ) ) ; try { const data = await promise ; if ( ! signal . aborted ) { setResult ( { data , status : FETCH_STATUS . SUCCESS , error : undefined , } as FetcherResult < InferResponseType < TReturn > > ) ; } } catch ( e ) { if ( ! signal . aborted ) { setResult ( ( prevResult ) => ( { data : preservePreviousData ? prevResult . data : undefined , status : FETCH_STATUS . FAILURE , error : e , loading : false , } ) ) ; } } } doFetch ( ) ; return ( ) => { controller . abort ( ) ; } ; } , [ counter , ... fnDeps ] ) ; return useMemo ( ( ) => { return { ... result , loading : result . status === FETCH_STATUS . LOADING || result . status === FETCH_STATUS . PENDING , refetch : ( ) => { setCounter ( ( count ) => count + 1 ) ; } , } ; } , [ result ] ) ; }