import { useEffect , useLayoutEffect , useReducer , useMemo , useContext , useCallback , useRef , } from 'react' ; import { ApiContext } from './context' ; import { initState , ACTIONS , axiosAll , getResponseData , isObject , isFunction , } from './common' ; export function useApi < D = ReactUseApi . SingleData > ( config , opt ? ) ; export function useApi < D = ReactUseApi . SingleData [ ] > ( config , opt ? ) ; export function useApi < D = ReactUseApi . Data > ( config , opt ? ) { if ( typeof config === 'string' ) { config = { url : config , } ; } const context = useContext ( ApiContext ) ; const { settings , settings : { cache , debug , clearLastCacheWhenConfigChanges } , isSSR , collection : { ssrConfigs , cacheKeys } , } = context ; const cacheKey = JSON . stringify ( config ) ; const ref = useRef ( useMemo ( ( ) => ( { id : Date . now ( ) , isRequesting : false , isInit : false , hasFed : false , refreshFlag : 1 , cacheKey , config , } as ReactUseApi . RefData ) , [ ] ) ) ; const options = useMemo ( ( ) => handleUseApiOptions ( opt , settings , config , cacheKey ) , [ opt , settings , cacheKey ] ) ; const { current : refData } = ref ; const isValidConfig = verifyConfig ( config ) ; const hasChangedConfig = refData . cacheKey !== cacheKey ; options . $hasChangedConfig = hasChangedConfig ; if ( hasChangedConfig ) { if ( clearLastCacheWhenConfigChanges ) { cache . del ( refData . cacheKey ) ; } refData . cacheKey = cacheKey ; refData . config = config ; refData . hasFed = false ; } const cacheData = cache . get ( cacheKey ) ; const { skip , useCache } = options ; let defaultState = { ... initState } ; if ( ! skip && ! refData . isInit ) { if ( cacheData && ! refData . hasFed && ( isSSR || useCache !== false ) ) { const { response , error } = cacheData ; const action = { type : ACTIONS . REQUEST_END , response , error , options , } as ReactUseApi . Action ; debug && console . log ( '[ReactUseApi][Feed]' , cacheKey ) ; if ( ! isSSR ) { action . fromCache = true ; refData . hasFed = true ; } defaultState = reducer ( defaultState , action ) ; } else if ( isSSR ) { if ( ! cacheKeys . has ( cacheKey ) ) { cacheKeys . add ( cacheKey ) ; debug && console . log ( '[ReactUseApi][Collect]' , cacheKey ) ; } ssrConfigs . push ( { config , cacheKey , } ) ; } } refData . isInit = true ; const [ state , dispatch ] = useReducer ( reducer , defaultState ) ; const { shouldRequest , watch } = options ; const { loading , data } = state ; const request = useCallback ( async ( cfg = refData . config as ReactUseApi . Config , keepState = false , revalidate = true ) => { if ( options . skip ) { return null ; } if ( ( cfg as React . MouseEvent ) ?. target && ( cfg as React . MouseEvent ) ?. isDefaultPrevented ) { cfg = refData . config ; } if ( keepState ) { state . $cacheKey = cacheKey ; } refData . isRequesting = true ; return fetchApi ( context , cfg , options , dispatch , revalidate ) ; } , [ context , cacheKey , options , dispatch , state , refData ] ) ; const shouldFetchApi = useCallback ( ( forRerender = false ) => { let shouldRequestResult ; if ( isFunction ( shouldRequest ) ) { shouldRequestResult = ! ! shouldRequest ( ) as boolean ; } return ( ! skip && ! refData . isRequesting && ( ( forRerender ? shouldRequestResult === true : shouldRequestResult !== false ) || hasChangedConfig ) ) ; } , [ skip , refData , shouldRequest , hasChangedConfig ] ) ; if ( shouldFetchApi ( true ) ) { refData . refreshFlag = Date . now ( ) ; } if ( ! loading && refData . isRequesting ) { refData . isRequesting = false ; } const useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect ; const effect = useCallback ( ( callback , ... args ) => { const wrapper = ( ) => { if ( isValidConfig ) { return callback ( ) ; } } ; return useIsomorphicLayoutEffect ( wrapper , ... args ) ; } , [ isValidConfig ] ) ; effect ( ( ) => { if ( ! isSSR && ! refData . hasFed ) { request ( undefined , undefined , false ) ; } } , [ cacheKey , useCache , refData , refData . refreshFlag , ... ( Array . isArray ( watch ) ? watch : [ ] ) , ] ) ; if ( ! isValidConfig ) { return [ undefined , undefined , undefined ] ; } return [ data , state , request ] ; } export const reducer = ( state , action ) : ReactUseApi . State => { const { type , options } = action ; const cacheKey = options . $cacheKey ; const basicState = { $cacheKey : cacheKey , } ; switch ( type ) { case ACTIONS . REQUEST_START : { return { ... ( cacheKey !== state . $cacheKey ? initState : state ) , loading : true , error : undefined , fromCache : false , ... basicState , } ; } case ACTIONS . REQUEST_END : { const { response , error , fromCache } = action ; const { prevState : pre , ... prevState } = state ; const { data : prevData } = prevState ; const { dependencies , $hasChangedConfig } = options ; const newState = { ... prevState , prevData , prevState , loading : false , response , dependencies , error , fromCache : ! ! fromCache , ... basicState , } ; if ( $hasChangedConfig ) { delete newState . prevState ; delete newState . prevData ; } newState . data = error ? undefined : getResponseData ( options , newState ) ; return newState ; } default : return state ; } } ; export const fetchApi = async ( context , config , options , dispatch , revalidate = false ) => { const { settings : { cache } , } = context ; const { $cacheKey : cacheKey , useCache } = options ; const promiseKey = `${ cacheKey }::promise` ; let { response , error } = { } as ReactUseApi . CacheData ; try { const cacheData = cache . get ( cacheKey ) ; let promise = cache . get ( promiseKey ) ; response = cacheData ?. response ; error = cacheData ?. error ; let fromCache = ! revalidate ; if ( revalidate ) { dispatch ( { type : ACTIONS . REQUEST_START , options } ) ; response = await axiosAll ( context , config ) ; } else if ( useCache !== false && promise ) { dispatch ( { type : ACTIONS . REQUEST_START , options } ) ; response = await promise ; } else if ( useCache !== false && ( response || error ) ) { } else if ( useCache ) { dispatch ( { type : ACTIONS . REQUEST_START , options } ) ; promise = axiosAll ( context , config ) ; cache . set ( promiseKey , promise ) ; response = await promise ; } else { fromCache = false ; dispatch ( { type : ACTIONS . REQUEST_START , options } ) ; response = await axiosAll ( context , config ) ; } dispatch ( { type : ACTIONS . REQUEST_END , response , error , options , fromCache } ) ; } catch ( err ) { error = err ; dispatch ( { type : ACTIONS . REQUEST_END , error , options , } ) ; } finally { if ( useCache ) { if ( ! cache . has ( cacheKey ) ) { cache . set ( cacheKey , { response , error , } ) ; } if ( cache . has ( promiseKey ) ) { cache . del ( promiseKey ) ; } } } } ; export const handleUseApiOptions = ( opt , settings , config , cacheKey ) => { const options = isObject ( opt ) ? ( { ... opt } as ReactUseApi . Options ) : ( { watch : Array . isArray ( opt ) ? opt : [ ] , handleData : isFunction ( opt ) ? opt : undefined , } as ReactUseApi . Options ) ; options . $cacheKey = cacheKey ; const { alwaysUseCache , shouldUseApiCache } = settings ; const globalUseCache = shouldUseApiCache ( config as ReactUseApi . Config , cacheKey ) !== false ; if ( alwaysUseCache ) { options . useCache = true ; } else if ( globalUseCache === false ) { options . useCache = false ; } return options ; } ; export const verifyConfig = ( config ) => { return isObject ( config ) ? ! ! ( config as ReactUseApi . SingleConfig ) . url : Array . isArray ( config ) ? config . every ( ( each ) => ! ! each . url ) : false ; } ; export default useApi ;