diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7775accb8..b2560ea02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: - run: yarn install - run: yarn types:check - run: yarn lint - - run: yarn build:core + - run: yarn build - run: yarn test env: CI: true diff --git a/src/types.ts b/src/types.ts index d73fd1c39..80029af56 100644 --- a/src/types.ts +++ b/src/types.ts @@ -168,8 +168,8 @@ export interface RevalidatorOptions { } export type Revalidator = ( - revalidateOpts: RevalidatorOptions -) => Promise + revalidateOpts?: RevalidatorOptions +) => Promise | void export interface Cache { get(key: Key): Data | null | undefined diff --git a/src/use-swr.ts b/src/use-swr.ts index e09ca1b78..711e905ef 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -1,7 +1,6 @@ import { useCallback, useRef, useDebugValue } from 'react' import defaultConfig from './utils/config' -import { provider as defaultProvider } from './utils/web-preset' -import { wrapCache } from './utils/cache' +import { wrapCache, SWRGlobalState, GlobalState } from './utils/cache' import { IS_SERVER, rAF, useIsomorphicLayoutEffect } from './utils/env' import { serialize } from './utils/serialize' import { isUndefined, UNDEFINED } from './utils/helper' @@ -22,48 +21,13 @@ import { Cache, ScopedMutator, SWRHook, + Revalidator, ProviderOptions } from './types' -type Revalidator = (...args: any[]) => void - // Generate strictly increasing timestamps. let __timestamp = 0 -// Global state used to deduplicate requests and store listeners -const SWRGlobalState = new WeakMap() -const getGlobalState = (cache: Cache) => { - if (!SWRGlobalState.has(cache)) { - SWRGlobalState.set(cache, [{}, {}, {}, {}, {}, {}, {}]) - } - return SWRGlobalState.get(cache) as [ - Record, // FOCUS_REVALIDATORS - Record, // RECONNECT_REVALIDATORS - Record, // CACHE_REVALIDATORS - Record, // MUTATION_TS - Record, // MUTATION_END_TS - Record, // CONCURRENT_PROMISES - Record // CONCURRENT_PROMISES_TS - ] -} - -function setupGlobalEvents(cache: Cache, _opts: Partial = {}) { - if (IS_SERVER) return - const opts = { ...defaultProvider, ..._opts } - const [FOCUS_REVALIDATORS, RECONNECT_REVALIDATORS] = getGlobalState(cache) - const revalidate = (revalidators: Record) => { - for (const key in revalidators) { - if (revalidators[key][0]) revalidators[key][0]() - } - } - - opts.setupOnFocus(() => revalidate(FOCUS_REVALIDATORS)) - opts.setupOnReconnect(() => revalidate(RECONNECT_REVALIDATORS)) -} - -// Setup DOM events listeners for `focus` and `reconnect` actions -setupGlobalEvents(defaultConfig.cache) - const broadcastState: Broadcaster = ( cache: Cache, key, @@ -72,7 +36,7 @@ const broadcastState: Broadcaster = ( isValidating, shouldRevalidate = false ) => { - const [, , CACHE_REVALIDATORS] = getGlobalState(cache) + const [, , CACHE_REVALIDATORS] = SWRGlobalState.get(cache) as GlobalState const updaters = CACHE_REVALIDATORS[key] const promises = [] if (updaters) { @@ -94,7 +58,9 @@ async function internalMutate( const [key, , keyErr] = serialize(_key) if (!key) return UNDEFINED - const [, , , MUTATION_TS, MUTATION_END_TS] = getGlobalState(cache) + const [, , , MUTATION_TS, MUTATION_END_TS] = SWRGlobalState.get( + cache + ) as GlobalState // if there is no new data to update, let's just revalidate the key if (isUndefined(_data)) { @@ -108,18 +74,14 @@ async function internalMutate( ) } - // update global timestamps - MUTATION_TS[key] = ++__timestamp - MUTATION_END_TS[key] = 0 - - // track timestamps before await asynchronously - const beforeMutationTs = MUTATION_TS[key] - let data: any, error: unknown - let isAsyncMutation = false + + // Update global timestamps. + const beforeMutationTs = (MUTATION_TS[key] = ++__timestamp) + MUTATION_END_TS[key] = 0 if (typeof _data === 'function') { - // `_data` is a function, call it passing current cache value + // `_data` is a function, call it passing current cache value. try { _data = (_data as MutatorCallback)(cache.get(key)) } catch (err) { @@ -130,8 +92,7 @@ async function internalMutate( } if (_data && typeof (_data as Promise).then === 'function') { - // `_data` is a promise - isAsyncMutation = true + // `_data` is a promise/thenable, resolve the final data. try { data = await _data } catch (err) { @@ -141,32 +102,25 @@ async function internalMutate( data = _data } - const shouldAbort = (): boolean | void => { - // check if other mutations have occurred since we've started this mutation - if (beforeMutationTs !== MUTATION_TS[key]) { - if (error) throw error - return true - } - } + // Check if other mutations have occurred since we've started this mutation. + const shouldAbort = beforeMutationTs !== MUTATION_TS[key] - // If there's a race we don't update cache or broadcast change, just return the data - if (shouldAbort()) return data + // If there's a race we don't update cache or broadcast change, just return the data. + if (shouldAbort) { + if (error) throw error + return data + } if (!isUndefined(data)) { // update cached data cache.set(key, data) } - // Always update or reset the error + // Always update or reset the error. cache.set(keyErr, error) // Reset the timestamp to mark the mutation has ended MUTATION_END_TS[key] = ++__timestamp - if (!isAsyncMutation) { - // We skip broadcasting if there's another mutation happened synchronously - if (shouldAbort()) return data - } - // Update existing SWR Hooks' internal states: return broadcastState( cache, @@ -185,9 +139,9 @@ async function internalMutate( // Add a callback function to a list of keyed revalidation functions and returns // the unregister function. const addRevalidator = ( - revalidators: Record, + revalidators: Record)[]>, key: string, - callback: Revalidator + callback: Revalidator | Updater ) => { if (!revalidators[key]) { revalidators[key] = [callback] @@ -223,6 +177,7 @@ export function useSWRHandler( refreshWhenHidden, refreshWhenOffline } = config + const [ FOCUS_REVALIDATORS, RECONNECT_REVALIDATORS, @@ -231,7 +186,7 @@ export function useSWRHandler( MUTATION_END_TS, CONCURRENT_PROMISES, CONCURRENT_PROMISES_TS - ] = getGlobalState(cache) + ] = SWRGlobalState.get(cache) as GlobalState // `key` is the identifier of the SWR `data` state. // `keyErr` and `keyValidating` are identifiers of `error` and `isValidating` @@ -248,11 +203,8 @@ export function useSWRHandler( const configRef = useRef(config) // Get the current state that SWR should return. - const resolveData = () => { - const cachedData = cache.get(key) - return isUndefined(cachedData) ? initialData : cachedData - } - const data = resolveData() + const cachedData = cache.get(key) + const data = isUndefined(cachedData) ? initialData : cachedData const error = cache.get(keyErr) // A revalidation must be triggered when mounted if: @@ -278,10 +230,7 @@ export function useSWRHandler( } const isValidating = resolveValidating() - const [stateRef, stateDependenciesRef, setState] = useStateWithDeps< - Data, - Error - >( + const [stateRef, stateDependencies, setState] = useStateWithDeps( { data, error, @@ -293,14 +242,15 @@ export function useSWRHandler( // The revalidation function is a carefully crafted wrapper of the original // `fetcher`, to correctly handle the many edge cases. const revalidate = useCallback( - async (revalidateOpts: RevalidatorOptions = {}): Promise => { + async (revalidateOpts?: RevalidatorOptions): Promise => { if (!key || !fn || unmountedRef.current || configRef.current.isPaused()) { return false } - const { retryCount, dedupe } = revalidateOpts - + let newData: Data + let startAt: number let loading = true + const { retryCount, dedupe } = revalidateOpts || {} const shouldDeduping = !isUndefined(CONCURRENT_PROMISES[key]) && dedupe // Do unmount check for callbacks: @@ -329,9 +279,6 @@ export function useSWRHandler( ) } - let newData: Data - let startAt: number - if (shouldDeduping) { // There's already an ongoing request, this one needs to be // deduplicated. @@ -362,8 +309,9 @@ export function useSWRHandler( // trigger the success event, // only do this for the original request. - if (isCallbackSafe()) + if (isCallbackSafe()) { configRef.current.onSuccess(newData, key, config) + } } // if there're other ongoing request(s), started after the current one, @@ -486,6 +434,16 @@ export function useSWRHandler( [key] ) + // `mutate`, but bound to the current key. + const boundMutate: SWRResponse['mutate'] = useCallback( + (newData, shouldRevalidate) => { + return internalMutate(cache, keyRef.current, newData, shouldRevalidate) + }, + // `cache` isn't allowed to change during the lifecycle + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ) + // Always update config. useIsomorphicLayoutEffect(() => { configRef.current = config @@ -525,13 +483,13 @@ export function useSWRHandler( } } - const isVisible = () => + const isActive = () => configRef.current.isDocumentVisible() && configRef.current.isOnline() // Add event listeners. let pending = false const onFocus = () => { - if (configRef.current.revalidateOnFocus && !pending && isVisible()) { + if (configRef.current.revalidateOnFocus && !pending && isActive()) { pending = true softRevalidate() setTimeout( @@ -541,8 +499,8 @@ export function useSWRHandler( } } - const onReconnect = () => { - if (configRef.current.revalidateOnReconnect && isVisible()) { + const onReconnect: Revalidator = () => { + if (configRef.current.revalidateOnReconnect && isActive()) { softRevalidate() } } @@ -625,6 +583,9 @@ export function useSWRHandler( } }, [refreshInterval, refreshWhenHidden, refreshWhenOffline, revalidate]) + // Display debug info in React DevTools. + useDebugValue(data) + // In Suspense mode, we can't return the empty `data` state. // If there is `error`, the `error` needs to be thrown to the error boundary. // If there is no `error`, the `revalidation` promise needs to be thrown to @@ -633,21 +594,6 @@ export function useSWRHandler( throw isUndefined(error) ? revalidate({ dedupe: true }) : error } - // `mutate`, but bound to the current key. - const boundMutate: SWRResponse['mutate'] = useCallback( - (newData, shouldRevalidate) => { - return internalMutate(cache, keyRef.current, newData, shouldRevalidate) - }, - // `cache` isn't allowed to change during the lifecycle - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ) - - // Display debug info in React DevTools. - useDebugValue(data) - - const currentStateDependencies = stateDependenciesRef.current - // Define the SWR state. // `revalidate` will be deprecated in the 1.x release // because `mutate()` covers the same use case of `revalidate()`. @@ -660,21 +606,21 @@ export function useSWRHandler( { data: { get: function() { - currentStateDependencies.data = true + stateDependencies.data = true return data }, enumerable: true }, error: { get: function() { - currentStateDependencies.error = true + stateDependencies.error = true return error }, enumerable: true }, isValidating: { get: function() { - currentStateDependencies.isValidating = true + stateDependencies.isValidating = true return isValidating }, enumerable: true @@ -690,7 +636,7 @@ export const SWRConfig = Object.defineProperty(ConfigProvider, 'default', { } export const mutate = internalMutate.bind( - null, + UNDEFINED, defaultConfig.cache ) as ScopedMutator @@ -701,11 +647,10 @@ export function createCache( cache: Cache mutate: ScopedMutator } { - const cache = wrapCache(provider) - setupGlobalEvents(cache, options) + const cache = wrapCache(provider, options) return { cache, - mutate: internalMutate.bind(null, cache) as ScopedMutator + mutate: internalMutate.bind(UNDEFINED, cache) as ScopedMutator } } diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 19dd7e5ce..f5175a5cb 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -1,6 +1,51 @@ -import { Cache } from '../types' +import { provider as defaultProvider } from './web-preset' +import { IS_SERVER } from './env' +import { UNDEFINED } from './helper' + +import { Cache, Revalidator, Updater, ProviderOptions } from '../types' + +export type GlobalState = [ + Record, // FOCUS_REVALIDATORS + Record, // RECONNECT_REVALIDATORS + Record)[]>, // CACHE_REVALIDATORS + Record, // MUTATION_TS + Record, // MUTATION_END_TS + Record, // CONCURRENT_PROMISES + Record // CONCURRENT_PROMISES_TS +] + +// Global state used to deduplicate requests and store listeners +export const SWRGlobalState = new WeakMap() + +function revalidateAllKeys(revalidators: Record) { + for (const key in revalidators) { + if (revalidators[key][0]) revalidators[key][0]() + } +} + +function setupGlobalEvents(cache: Cache, options: ProviderOptions) { + const [FOCUS_REVALIDATORS, RECONNECT_REVALIDATORS] = SWRGlobalState.get( + cache + ) as GlobalState + options.setupOnFocus(revalidateAllKeys.bind(UNDEFINED, FOCUS_REVALIDATORS)) + options.setupOnReconnect( + revalidateAllKeys.bind(UNDEFINED, RECONNECT_REVALIDATORS) + ) +} + +export function wrapCache( + provider: Cache, + options?: Partial +): Cache { + // Initialize global state for the specific data storage that will be used to + // deduplicate requests and store listeners. + SWRGlobalState.set(provider, [{}, {}, {}, {}, {}, {}, {}]) + + // Setup DOM events listeners for `focus` and `reconnect` actions. + if (!IS_SERVER) { + setupGlobalEvents(provider, { ...defaultProvider, ...options }) + } -export function wrapCache(provider: Cache): Cache { // We might want to inject an extra layer on top of `provider` in the future, // such as key serialization, auto GC, etc. // For now, it's just a `Map` interface without any modifications. diff --git a/src/utils/state.ts b/src/utils/state.ts index ae77c9da7..501647dd9 100644 --- a/src/utils/state.ts +++ b/src/utils/state.ts @@ -12,11 +12,7 @@ type StateDeps = Record export default function useStateWithDeps>( state: S, unmountedRef: MutableRefObject -): [ - MutableRefObject, - MutableRefObject>, - (payload: S) => void -] { +): [MutableRefObject, Record, (payload: S) => void] { const rerender = useState>({})[1] const stateRef = useRef(state) @@ -84,5 +80,5 @@ export default function useStateWithDeps>( stateRef.current = state }) - return [stateRef, stateDependenciesRef, setState] + return [stateRef, stateDependenciesRef.current, setState] }