diff --git a/src/config.ts b/src/config.ts index 31cc5dec7..fbb87002f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,5 @@ import { dequal } from 'dequal/lite' -import { SWRConfiguration, RevalidatorOptions, Revalidator } from './types' +import { Configuration, RevalidatorOptions, Revalidator } from './types' import Cache from './cache' import webPreset from './libs/web-preset' @@ -8,11 +8,11 @@ const cache = new Cache() // error retry function onErrorRetry( - _, - __, - config: SWRConfiguration, + _: unknown, + __: string, + config: Readonly>, revalidate: Revalidator, - opts: RevalidatorOptions + opts: Required ): void { if (!config.isDocumentVisible()) { // if it's hidden, stop @@ -28,7 +28,7 @@ function onErrorRetry( } // exponential backoff - const count = Math.min(opts.retryCount || 0, 8) + const count = Math.min(opts.retryCount, 8) const timeout = ~~((Math.random() + 0.5) * (1 << count)) * config.errorRetryInterval setTimeout(revalidate, timeout, opts) @@ -39,36 +39,36 @@ function onErrorRetry( // slow connection (<= 70Kbps) const slowConnection = typeof window !== 'undefined' && + // @ts-ignore navigator['connection'] && + // @ts-ignore ['slow-2g', '2g'].indexOf(navigator['connection'].effectiveType) !== -1 // config -const defaultConfig: SWRConfiguration = Object.assign( - { - // events - onLoadingSlow: () => {}, - onSuccess: () => {}, - onError: () => {}, - onErrorRetry, +const defaultConfig = { + // events + onLoadingSlow: () => {}, + onSuccess: () => {}, + onError: () => {}, + onErrorRetry, - errorRetryInterval: (slowConnection ? 10 : 5) * 1000, - focusThrottleInterval: 5 * 1000, - dedupingInterval: 2 * 1000, - loadingTimeout: (slowConnection ? 5 : 3) * 1000, + errorRetryInterval: (slowConnection ? 10 : 5) * 1000, + focusThrottleInterval: 5 * 1000, + dedupingInterval: 2 * 1000, + loadingTimeout: (slowConnection ? 5 : 3) * 1000, - refreshInterval: 0, - revalidateOnFocus: true, - revalidateOnReconnect: true, - refreshWhenHidden: false, - refreshWhenOffline: false, - shouldRetryOnError: true, - suspense: false, - compare: dequal, + refreshInterval: 0, + revalidateOnFocus: true, + revalidateOnReconnect: true, + refreshWhenHidden: false, + refreshWhenOffline: false, + shouldRetryOnError: true, + suspense: false, + compare: dequal, - isPaused: () => false - }, - webPreset -) + isPaused: () => false, + ...webPreset +} as const export { cache } export default defaultConfig diff --git a/src/libs/web-preset.ts b/src/libs/web-preset.ts index c57ed7be2..f31d4287a 100644 --- a/src/libs/web-preset.ts +++ b/src/libs/web-preset.ts @@ -19,7 +19,7 @@ const isDocumentVisible = () => { return true } -const fetcher = url => fetch(url).then(res => res.json()) +const fetcher = (url: string) => fetch(url).then(res => res.json()) const registerOnFocus = (cb: () => void) => { if ( diff --git a/src/types.ts b/src/types.ts index b31c86d1c..d4a60c9a6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,12 +1,9 @@ -// Internal types - export type Fetcher = (...args: any) => Data | Promise - -export type Configuration< +export interface Configuration< Data = any, Error = any, Fn extends Fetcher = Fetcher -> = { +> { errorRetryInterval: number errorRetryCount?: number loadingTimeout: number @@ -26,19 +23,26 @@ export type Configuration< isOnline: () => boolean isDocumentVisible: () => boolean isPaused: () => boolean - onLoadingSlow: (key: string, config: Configuration) => void + onLoadingSlow: ( + key: string, + config: Readonly>> + ) => void onSuccess: ( data: Data, key: string, - config: Configuration + config: Readonly>> + ) => void + onError: ( + err: Error, + key: string, + config: Readonly>> ) => void - onError: (err: Error, key: string, config: Configuration) => void onErrorRetry: ( err: Error, key: string, - config: Configuration, + config: Readonly>>, revalidate: Revalidator, - revalidateOpts: RevalidatorOptions + revalidateOpts: Required ) => void registerOnFocus?: (cb: () => void) => void registerOnReconnect?: (cb: () => void) => void @@ -57,7 +61,7 @@ export type Updater = ( ) => boolean | Promise export type Trigger = (key: Key, shouldRevalidate?: boolean) => Promise -type MutatorCallback = ( +export type MutatorCallback = ( currentValue: undefined | Data ) => Promise | undefined | Data @@ -116,7 +120,7 @@ export type responseInterface = { ) => Promise isValidating: boolean } -export type SWRResponse = { +export interface SWRResponse { data?: Data error?: Error revalidate: () => Promise @@ -159,10 +163,8 @@ export type SWRInfiniteResponseInterface = SWRResponse< size: number | ((size: number) => number) ) => Promise } -export type SWRInfiniteResponse = SWRResponse< - Data[], - Error -> & { +export interface SWRInfiniteResponse + extends SWRResponse { size: number setSize: ( size: number | ((size: number) => number) diff --git a/src/use-swr-infinite.ts b/src/use-swr-infinite.ts index e0277cd08..0d6e6ed51 100644 --- a/src/use-swr-infinite.ts +++ b/src/use-swr-infinite.ts @@ -1,3 +1,4 @@ +// TODO: use @ts-expect-error import { useContext, useRef, useState, useEffect, useCallback } from 'react' import defaultConfig, { cache } from './config' @@ -8,7 +9,8 @@ import { ValueKey, Fetcher, SWRInfiniteConfiguration, - SWRInfiniteResponse + SWRInfiniteResponse, + MutatorCallback } from './types' type KeyLoader = ( @@ -17,51 +19,44 @@ type KeyLoader = ( ) => ValueKey function useSWRInfinite( - getKey: KeyLoader -): SWRInfiniteResponse -function useSWRInfinite( - getKey: KeyLoader, - config?: Partial> -): SWRInfiniteResponse -function useSWRInfinite( - getKey: KeyLoader, - fn?: Fetcher, - config?: Partial> -): SWRInfiniteResponse -function useSWRInfinite( - getKey: KeyLoader, - ...options: any[] + ...args: + | readonly [KeyLoader] + | readonly [KeyLoader, Fetcher] + | readonly [KeyLoader, SWRInfiniteConfiguration] + | readonly [ + KeyLoader, + Fetcher, + SWRInfiniteConfiguration + ] ): SWRInfiniteResponse { - let _fn: Fetcher | undefined, - _config: Partial> = {} - - if (options.length > 1) { - _fn = options[0] - _config = options[1] - } else { - if (typeof options[0] === 'function') { - _fn = options[0] - } else if (typeof options[0] === 'object') { - _config = options[0] - } - } + const getKey = args[0] - const config: SWRInfiniteConfiguration = Object.assign( + const config = Object.assign( {}, defaultConfig, useContext(SWRConfigContext), - _config + args.length > 2 + ? args[2] + : args.length === 2 && typeof args[1] === 'object' + ? args[1] + : {} ) - let { + // in typescript args.length > 2 is not same as args.lenth === 3 + // we do a safe type assertion here + // args.length === 3 + const fn = (args.length > 2 + ? args[1] + : args.length === 2 && typeof args[1] === 'function' + ? args[1] + : config.fetcher) as Fetcher + + const { initialSize = 1, revalidateAll = false, persistSize = false, - fetcher: defaultFetcher, ...extraConfig } = config - const fn = typeof _fn !== 'undefined' ? _fn : defaultFetcher - // get the serialized key of the first page let firstPageKey: string | null = null try { @@ -172,7 +167,7 @@ function useSWRInfinite( }, [swr.data]) const mutate = useCallback( - (data, shouldRevalidate = true) => { + (data: MutatorCallback, shouldRevalidate = true) => { if (shouldRevalidate && typeof data !== 'undefined') { // we only revalidate the pages that are changed const originalData = dataRef.current @@ -228,7 +223,7 @@ function useSWRInfinite( enumerable: true } }) - return swrInfinite as SWRInfiniteResponse + return (swrInfinite as unknown) as SWRInfiniteResponse } export { useSWRInfinite } diff --git a/src/use-swr.ts b/src/use-swr.ts index c065960e2..637bac3d5 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -1,3 +1,4 @@ +// TODO: use @ts-expect-error import { useCallback, useContext, @@ -14,15 +15,14 @@ import SWRConfigContext from './swr-config-context' import { Action, Broadcaster, - Configuration, - SWRConfiguration, Fetcher, Key, Mutator, SWRResponse, RevalidatorOptions, Trigger, - Updater + Updater, + SWRConfiguration } from './types' const IS_SERVER = @@ -40,14 +40,16 @@ const rAF = IS_SERVER // useLayoutEffect in the browser. const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect +type Revalidator = (...args: any[]) => void + // global state managers -const CONCURRENT_PROMISES = {} -const CONCURRENT_PROMISES_TS = {} -const FOCUS_REVALIDATORS = {} -const RECONNECT_REVALIDATORS = {} -const CACHE_REVALIDATORS = {} -const MUTATION_TS = {} -const MUTATION_END_TS = {} +const CONCURRENT_PROMISES: Record = {} +const CONCURRENT_PROMISES_TS: Record = {} +const FOCUS_REVALIDATORS: Record = {} +const RECONNECT_REVALIDATORS: Record = {} +const CACHE_REVALIDATORS: Record = {} +const MUTATION_TS: Record = {} +const MUTATION_END_TS: Record = {} // generate strictly increasing timestamps const now = (() => { @@ -57,7 +59,7 @@ const now = (() => { // setup DOM events listeners for `focus` and `reconnect` actions if (!IS_SERVER) { - const revalidate = revalidators => { + const revalidate = (revalidators: Record) => { if (!defaultConfig.isDocumentVisible() || !defaultConfig.isOnline()) return for (const key in revalidators) { @@ -128,7 +130,7 @@ const mutate: Mutator = async (_key, _data, shouldRevalidate = true) => { const beforeMutationTs = MUTATION_TS[key] const beforeConcurrentPromisesTs = CONCURRENT_PROMISES_TS[key] - let data, error + let data: any, error: unknown let isAsyncMutation = false if (_data && typeof _data === 'function') { @@ -204,34 +206,39 @@ const mutate: Mutator = async (_key, _data, shouldRevalidate = true) => { return data } -function useSWR(key: Key): SWRResponse -function useSWR( - key: Key, - config?: SWRConfiguration -): SWRResponse -function useSWR( - key: Key, - // `null` is used for a hack to manage shared state with SWR - // https://github.com/vercel/swr/pull/918 - fn?: Fetcher | null, - config?: SWRConfiguration -): SWRResponse function useSWR( - _key: Key, - ...options: any[] + ...args: + | readonly [Key] + | readonly [Key, Fetcher | null] + | readonly [Key, SWRConfiguration] + | readonly [Key, Fetcher | null, SWRConfiguration] ): SWRResponse { - let _fn: Fetcher | undefined, - _config: SWRConfiguration = {} - if (options.length > 1) { - _fn = options[0] - _config = options[1] - } else { - if (typeof options[0] === 'function') { - _fn = options[0] - } else if (typeof options[0] === 'object') { - _config = options[0] - } - } + const _key = args[0] + const config = Object.assign( + {}, + defaultConfig, + useContext(SWRConfigContext), + args.length > 2 + ? args[2] + : args.length === 2 && typeof args[1] === 'object' + ? args[1] + : {} + ) + + // in typescript args.length > 2 is not same as args.lenth === 3 + // we do a safe type assertion here + // args.length === 3 + const fn = (args.length > 2 + ? args[1] + : args.length === 2 && typeof args[1] === 'function' + ? args[1] + : /** + pass fn as null will disable revalidate + https://paco.sh/blog/shared-hook-state-with-swr + */ + args[1] === null + ? args[1] + : config.fetcher) as Fetcher | null // we assume `key` as the identifier of the request // `key` can change but `fn` shouldn't @@ -239,20 +246,11 @@ function useSWR( // `keyErr` is the cache key for error objects const [key, fnArgs, keyErr, keyValidating] = cache.serializeKey(_key) - const config = Object.assign( - {}, - defaultConfig, - useContext(SWRConfigContext), - _config - ) as Configuration - const configRef = useRef(config) useIsomorphicLayoutEffect(() => { configRef.current = config }) - const fn = typeof _fn !== 'undefined' ? _fn : config.fetcher - const willRevalidateOnMount = () => { return ( config.revalidateOnMount || @@ -290,16 +288,19 @@ function useSWR( // display the data label in the React DevTools next to SWR hooks useDebugValue(stateRef.current.data) - const [, rerender] = useState(null) + const rerender = useState(null)[1] + let dispatch = useCallback( (payload: Action) => { let shouldUpdateState = false for (let k in payload) { + // @ts-ignore if (stateRef.current[k] === payload[k]) { continue } - + // @ts-ignore stateRef.current[k] = payload[k] + // @ts-ignore if (stateDependencies.current[k]) { shouldUpdateState = true } @@ -330,6 +331,7 @@ function useSWR( if (unmountedRef.current) return if (!initialMountedRef.current) return if (key !== keyRef.current) return + // @ts-ignore configRef.current[event](...params) }, [key] @@ -342,7 +344,10 @@ function useSWR( [] ) - const addRevalidator = (revalidators, callback) => { + const addRevalidator = ( + revalidators: Record, + callback: Revalidator + ) => { if (!callback) return if (!revalidators[key]) { revalidators[key] = [callback] @@ -351,7 +356,10 @@ function useSWR( } } - const removeRevalidator = (revlidators, callback) => { + const removeRevalidator = ( + revlidators: Record, + callback: Revalidator + ) => { if (revlidators[key]) { const revalidators = revlidators[key] const index = revalidators.indexOf(callback) @@ -370,11 +378,11 @@ function useSWR( if (!key || !fn) return false if (unmountedRef.current) return false if (configRef.current.isPaused()) return false - revalidateOpts = Object.assign({ dedupe: false }, revalidateOpts) + const { retryCount = 0, dedupe = false } = revalidateOpts let loading = true let shouldDeduping = - typeof CONCURRENT_PROMISES[key] !== 'undefined' && revalidateOpts.dedupe + typeof CONCURRENT_PROMISES[key] !== 'undefined' && dedupe // start fetching try { @@ -520,15 +528,10 @@ function useSWR( eventsCallback('onError', err, key, config) if (config.shouldRetryOnError) { // when retrying, we always enable deduping - const retryCount = (revalidateOpts.retryCount || 0) + 1 - eventsCallback( - 'onErrorRetry', - err, - key, - config, - revalidate, - Object.assign({ dedupe: true }, revalidateOpts, { retryCount }) - ) + eventsCallback('onErrorRetry', err, key, config, revalidate, { + retryCount: retryCount + 1, + dedupe: true + }) } } @@ -580,6 +583,8 @@ function useSWR( if (typeof latestKeyedData !== 'undefined' && !IS_SERVER) { // delay revalidate if there's cache // to not block the rendering + + //@ts-ignore it's safe to use requestAnimationFrame in browser rAF(softRevalidate) } else { softRevalidate() @@ -670,7 +675,7 @@ function useSWR( }, [key, revalidate]) useIsomorphicLayoutEffect(() => { - let timer = null + let timer: any = null const tick = async () => { if ( !stateRef.current.error && @@ -705,8 +710,8 @@ function useSWR( ]) // suspense - let latestData - let latestError + let latestData: Data | undefined + let latestError: unknown if (config.suspense) { // in suspense mode, we can't return empty state // (it should be suspended) diff --git a/tsconfig.json b/tsconfig.json index 56de22606..0cddd9722 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,9 +3,7 @@ "declaration": true, "esModuleInterop": true, "jsx": "react", - "lib": [ - "esnext", "dom" - ], + "lib": ["esnext", "dom"], "module": "commonjs", "moduleResolution": "node", "noFallthroughCasesInSwitch": true, @@ -13,24 +11,16 @@ "noUnusedLocals": true, "noUnusedParameters": true, "outDir": "./dist", - "types": [ - "node", "jest" - ], + "types": ["node", "jest"], "rootDir": "src", + "strict": true, "target": "es5", - "typeRoots": [ - "./types", - "./node_modules/@types" - ] + "typeRoots": ["./types", "./node_modules/@types"] }, "include": ["src/**/*"], "watchOptions": { - // Use native file system events for files and directories "watchFile": "useFsEvents", "watchDirectory": "useFsEvents", - - // Poll files for updates more frequently - // when they're updated a lot. "fallbackPolling": "dynamicPriority" } }