import { useCallback , useRef , useDebugValue } from 'react' ; import { defaultConfig } from './utils/config' ; import { SWRGlobalState , GlobalState } from './utils/global-state' ; import { IS_SERVER , rAF , useIsomorphicLayoutEffect } from './utils/env' ; import { serialize } from './utils/serialize' ; import { isUndefined , UNDEFINED , OBJECT , mergeObjects , isFunction } from './utils/helper' ; import ConfigProvider from './utils/config-context' ; import { useStateWithDeps } from './utils/state' ; import { withArgs } from './utils/resolve-args' ; import { subscribeCallback } from './utils/subscribe-key' ; import { broadcastState } from './utils/broadcast-state' ; import { getTimestamp } from './utils/timestamp' ; import { internalMutate } from './utils/mutate' ; import * as revalidateEvents from './constants/revalidate-events' ; import { State , Fetcher , Key , SWRResponse , RevalidatorOptions , FullConfiguration , SWRConfiguration , SWRHook , StateUpdateCallback , RevalidateEvent } from './types' ; const WITH_DEDUPE = { dedupe : true } ; export const useSWRHandler = < Data = any , Error = any > ( _key , fetcher , config ) => { const { cache , compare , fallbackData , suspense , revalidateOnMount , refreshInterval , refreshWhenHidden , refreshWhenOffline } = config ; const [ EVENT_REVALIDATORS , STATE_UPDATERS , MUTATION , FETCH ] = SWRGlobalState . get ( cache ) as GlobalState ; const [ key , fnArgs , keyInfo ] = serialize ( _key ) ; const initialMountedRef = useRef ( false ) ; const unmountedRef = useRef ( false ) ; const keyRef = useRef ( key ) ; const fetcherRef = useRef ( fetcher ) ; const configRef = useRef ( config ) ; const getConfig = ( ) => configRef . current ; const isActive = ( ) => getConfig ( ) . isVisible ( ) && getConfig ( ) . isOnline ( ) ; const patchFetchInfo = ( info ) => cache . set ( keyInfo , mergeObjects ( cache . get ( keyInfo ) , info ) ) ; const cached = cache . get ( key ) ; const fallback = isUndefined ( fallbackData ) ? config . fallback [ key ] : fallbackData ; const data = isUndefined ( cached ) ? fallback : cached ; const info = cache . get ( keyInfo ) || { } ; const error = info . error ; const shouldRevalidateOnMount = ( ) => { if ( ! isUndefined ( revalidateOnMount ) ) return revalidateOnMount ; if ( getConfig ( ) . isPaused ( ) ) return false ; return suspense ? ! isUndefined ( data ) : isUndefined ( data ) || config . revalidateIfStale ; } ; const resolveValidating = ( ) => { if ( ! key || ! fetcher ) return false ; if ( info . isValidating ) return true ; return ! initialMountedRef . current && shouldRevalidateOnMount ( ) ; } ; const isValidating = resolveValidating ( ) ; const [ stateRef , stateDependencies , setState ] = useStateWithDeps < Data , Error > ( { data , error , isValidating } , unmountedRef ) ; const revalidate = useCallback ( async ( revalidateOpts ? ) : Promise < boolean > => { const currentFetcher = fetcherRef . current ; if ( ! key || ! currentFetcher || unmountedRef . current || getConfig ( ) . isPaused ( ) ) { return false ; } let newData ; let startAt ; let loading = true ; const opts = revalidateOpts || { } ; const shouldStartNewRequest = ! FETCH [ key ] || ! opts . dedupe ; const isCurrentKeyMounted = ( ) => ! unmountedRef . current && key === keyRef . current && initialMountedRef . current ; const cleanupState = ( ) => { const requestInfo = FETCH [ key ] ; if ( requestInfo && requestInfo [ 1 ] === startAt ) { delete FETCH [ key ] ; } } ; const newState = { isValidating : false } ; const finishRequestAndUpdateState = ( ) => { patchFetchInfo ( { isValidating : false } ) ; if ( isCurrentKeyMounted ( ) ) { setState ( newState ) ; } } ; patchFetchInfo ( { isValidating : true } ) ; setState ( { isValidating : true } ) ; try { if ( shouldStartNewRequest ) { broadcastState ( cache , key , stateRef . current . data , stateRef . current . error , true ) ; if ( config . loadingTimeout && ! cache . get ( key ) ) { setTimeout ( ( ) => { if ( loading && isCurrentKeyMounted ( ) ) { getConfig ( ) . onLoadingSlow ( key , config ) ; } } , config . loadingTimeout ) ; } FETCH [ key ] = [ currentFetcher ( ... fnArgs ) , getTimestamp ( ) ] ; } ; [ newData , startAt ] = FETCH [ key ] ; newData = await newData ; if ( shouldStartNewRequest ) { setTimeout ( cleanupState , config . dedupingInterval ) ; } if ( ! FETCH [ key ] || FETCH [ key ] [ 1 ] !== startAt ) { if ( shouldStartNewRequest ) { if ( isCurrentKeyMounted ( ) ) { getConfig ( ) . onDiscarded ( key ) ; } } return false ; } patchFetchInfo ( { error : UNDEFINED } ) ; newState . error = UNDEFINED ; const mutationInfo = MUTATION [ key ] ; if ( ! isUndefined ( mutationInfo ) && ( startAt <= mutationInfo [ 0 ] || startAt <= mutationInfo [ 1 ] || mutationInfo [ 1 ] === 0 ) ) { finishRequestAndUpdateState ( ) ; if ( shouldStartNewRequest ) { if ( isCurrentKeyMounted ( ) ) { getConfig ( ) . onDiscarded ( key ) ; } } return false ; } if ( ! compare ( stateRef . current . data , newData ) ) { newState . data = newData ; } else { newState . data = stateRef . current . data ; } if ( ! compare ( cache . get ( key ) , newData ) ) { cache . set ( key , newData ) ; } if ( shouldStartNewRequest ) { if ( isCurrentKeyMounted ( ) ) { getConfig ( ) . onSuccess ( newData , key , config ) ; } } } catch ( err ) { cleanupState ( ) ; if ( ! getConfig ( ) . isPaused ( ) ) { patchFetchInfo ( { error : err } ) ; newState . error = err as Error ; if ( shouldStartNewRequest && isCurrentKeyMounted ( ) ) { getConfig ( ) . onError ( err , key , config ) ; if ( config . shouldRetryOnError ) { if ( isActive ( ) ) { getConfig ( ) . onErrorRetry ( err , key , config , revalidate , { retryCount : ( opts . retryCount || 0 ) + 1 , dedupe : true } ) ; } } } } } loading = false ; finishRequestAndUpdateState ( ) ; if ( isCurrentKeyMounted ( ) && shouldStartNewRequest ) { broadcastState ( cache , key , newState . data , newState . error , false ) ; } return true ; } , [ key ] ) ; const boundMutate = useCallback ( internalMutate . bind ( UNDEFINED , cache , ( ) => keyRef . current ) , [ ] ) ; useIsomorphicLayoutEffect ( ( ) => { fetcherRef . current = fetcher ; configRef . current = config ; } ) ; useIsomorphicLayoutEffect ( ( ) => { if ( ! key ) return ; const keyChanged = initialMountedRef . current ; const softRevalidate = revalidate . bind ( UNDEFINED , WITH_DEDUPE ) ; const onStateUpdate = ( updatedData , updatedError , updatedIsValidating ) => { setState ( mergeObjects ( { error : updatedError , isValidating : updatedIsValidating } , compare ( stateRef . current . data , updatedData ) ? UNDEFINED : { data : updatedData } ) ) ; } ; let nextFocusRevalidatedAt = 0 ; const onRevalidate = ( type ) => { if ( type == revalidateEvents . FOCUS_EVENT ) { const now = Date . now ( ) ; if ( getConfig ( ) . revalidateOnFocus && now > nextFocusRevalidatedAt && isActive ( ) ) { nextFocusRevalidatedAt = now + getConfig ( ) . focusThrottleInterval ; softRevalidate ( ) ; } } else if ( type == revalidateEvents . RECONNECT_EVENT ) { if ( getConfig ( ) . revalidateOnReconnect && isActive ( ) ) { softRevalidate ( ) ; } } else if ( type == revalidateEvents . MUTATE_EVENT ) { return revalidate ( ) ; } return ; } ; const unsubUpdate = subscribeCallback ( key , STATE_UPDATERS , onStateUpdate ) ; const unsubEvents = subscribeCallback ( key , EVENT_REVALIDATORS , onRevalidate ) ; unmountedRef . current = false ; keyRef . current = key ; initialMountedRef . current = true ; if ( keyChanged ) { setState ( { data , error , isValidating } ) ; } if ( shouldRevalidateOnMount ( ) ) { if ( isUndefined ( data ) || IS_SERVER ) { softRevalidate ( ) ; } else { rAF ( softRevalidate ) ; } } return ( ) => { unmountedRef . current = true ; unsubUpdate ( ) ; unsubEvents ( ) ; } ; } , [ key , revalidate ] ) ; useIsomorphicLayoutEffect ( ( ) => { let timer ; function next ( ) { const interval = isFunction ( refreshInterval ) ? refreshInterval ( data ) : refreshInterval ; if ( interval && timer !== - 1 ) { timer = setTimeout ( execute , interval ) ; } } function execute ( ) { if ( ! stateRef . current . error && ( refreshWhenHidden || getConfig ( ) . isVisible ( ) ) && ( refreshWhenOffline || getConfig ( ) . isOnline ( ) ) ) { revalidate ( WITH_DEDUPE ) . then ( next ) ; } else { next ( ) ; } } next ( ) ; return ( ) => { if ( timer ) { clearTimeout ( timer ) ; timer = - 1 ; } } ; } , [ refreshInterval , refreshWhenHidden , refreshWhenOffline , revalidate ] ) ; useDebugValue ( data ) ; if ( suspense && isUndefined ( data ) && key ) { throw isUndefined ( error ) ? revalidate ( WITH_DEDUPE ) : error ; } return { mutate : boundMutate , get data ( ) { stateDependencies . data = true ; return data ; } , get error ( ) { stateDependencies . error = true ; return error ; } , get isValidating ( ) { stateDependencies . isValidating = true ; return isValidating ; } } as SWRResponse < Data , Error > ; } ; export const SWRConfig = OBJECT . defineProperty ( ConfigProvider , 'default' , { value : defaultConfig } ) as typeof ConfigProvider & { default : FullConfiguration ; } ; export const unstable_serialize = ( key ) => serialize ( key ) [ 0 ] ; export default withArgs < SWRHook > ( useSWRHandler ) ;