UNPKG

swr

Version:

React Hooks library for remote data fetching

616 lines (598 loc) 23.5 kB
'use client'; var React = require('react'); var revalidateEvents = require('./events.js'); var lite = require('dequal/lite'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return n; } var React__default = /*#__PURE__*/_interopDefault(React); var revalidateEvents__namespace = /*#__PURE__*/_interopNamespace(revalidateEvents); // Global state used to deduplicate requests and store listeners const SWRGlobalState = new WeakMap(); // Shared state between server components and client components const noop = ()=>{}; // Using noop() as the undefined value as undefined can be replaced // by something else. Prettier ignore and extra parentheses are necessary here // to ensure that tsc doesn't remove the __NOINLINE__ comment. // prettier-ignore const UNDEFINED = /*#__NOINLINE__*/ noop(); const OBJECT = Object; const isUndefined = (v)=>v === UNDEFINED; const isFunction = (v)=>typeof v == 'function'; const mergeObjects = (a, b)=>({ ...a, ...b }); const isPromiseLike = (x)=>isFunction(x.then); const EMPTY_CACHE = {}; const INITIAL_CACHE = {}; const STR_UNDEFINED = 'undefined'; // NOTE: Use the function to guarantee it's re-evaluated between jsdom and node runtime for tests. const isWindowDefined = typeof window != STR_UNDEFINED; const isDocumentDefined = typeof document != STR_UNDEFINED; const isLegacyDeno = isWindowDefined && 'Deno' in window; const hasRequestAnimationFrame = ()=>isWindowDefined && typeof window['requestAnimationFrame'] != STR_UNDEFINED; const createCacheHelper = (cache, key)=>{ const state = SWRGlobalState.get(cache); return [ // Getter ()=>!isUndefined(key) && cache.get(key) || EMPTY_CACHE, // Setter (info)=>{ if (!isUndefined(key)) { const prev = cache.get(key); // Before writing to the store, we keep the value in the initial cache // if it's not there yet. if (!(key in INITIAL_CACHE)) { INITIAL_CACHE[key] = prev; } state[5](key, mergeObjects(prev, info), prev || EMPTY_CACHE); } }, // Subscriber state[6], // Get server cache snapshot ()=>{ if (!isUndefined(key)) { // If the cache was updated on the client, we return the stored initial value. if (key in INITIAL_CACHE) return INITIAL_CACHE[key]; } // If we haven't done any client-side updates, we return the current value. return !isUndefined(key) && cache.get(key) || EMPTY_CACHE; } ]; } // export { UNDEFINED, OBJECT, isUndefined, isFunction, mergeObjects, isPromiseLike } ; /** * Due to the bug https://bugs.chromium.org/p/chromium/issues/detail?id=678075, * it's not reliable to detect if the browser is currently online or offline * based on `navigator.onLine`. * As a workaround, we always assume it's online on the first load, and change * the status upon `online` or `offline` events. */ let online = true; const isOnline = ()=>online; // For node and React Native, `add/removeEventListener` doesn't exist on window. const [onWindowEvent, offWindowEvent] = isWindowDefined && window.addEventListener ? [ window.addEventListener.bind(window), window.removeEventListener.bind(window) ] : [ noop, noop ]; const isVisible = ()=>{ const visibilityState = isDocumentDefined && document.visibilityState; return isUndefined(visibilityState) || visibilityState !== 'hidden'; }; const initFocus = (callback)=>{ // focus revalidate if (isDocumentDefined) { document.addEventListener('visibilitychange', callback); } onWindowEvent('focus', callback); return ()=>{ if (isDocumentDefined) { document.removeEventListener('visibilitychange', callback); } offWindowEvent('focus', callback); }; }; const initReconnect = (callback)=>{ // revalidate on reconnected const onOnline = ()=>{ online = true; callback(); }; // nothing to revalidate, just update the status const onOffline = ()=>{ online = false; }; onWindowEvent('online', onOnline); onWindowEvent('offline', onOffline); return ()=>{ offWindowEvent('online', onOnline); offWindowEvent('offline', onOffline); }; }; const preset = { isOnline, isVisible }; const defaultConfigOptions = { initFocus, initReconnect }; const IS_REACT_LEGACY = !React__default.default.useId; const IS_SERVER = !isWindowDefined || isLegacyDeno; // Polyfill requestAnimationFrame const rAF = (f)=>hasRequestAnimationFrame() ? window['requestAnimationFrame'](f) : setTimeout(f, 1); // React currently throws a warning when using useLayoutEffect on the server. // To get around it, we can conditionally useEffect on the server (no-op) and // useLayoutEffect in the browser. const useIsomorphicLayoutEffect = IS_SERVER ? React.useEffect : React.useLayoutEffect; // This assignment is to extend the Navigator type to use effectiveType. const navigatorConnection = typeof navigator !== 'undefined' && navigator.connection; // Adjust the config based on slow connection status (<= 70Kbps). const slowConnection = !IS_SERVER && navigatorConnection && ([ 'slow-2g', '2g' ].includes(navigatorConnection.effectiveType) || navigatorConnection.saveData); // use WeakMap to store the object->key mapping // so the objects can be garbage collected. // WeakMap uses a hashtable under the hood, so the lookup // complexity is almost O(1). const table = new WeakMap(); const isObjectType = (value, type)=>OBJECT.prototype.toString.call(value) === `[object ${type}]`; // counter of the key let counter = 0; // A stable hash implementation that supports: // - Fast and ensures unique hash properties // - Handles unserializable values // - Handles object key ordering // - Generates short results // // This is not a serialization function, and the result is not guaranteed to be // parsable. const stableHash = (arg)=>{ const type = typeof arg; const isDate = isObjectType(arg, 'Date'); const isRegex = isObjectType(arg, 'RegExp'); const isPlainObject = isObjectType(arg, 'Object'); let result; let index; if (OBJECT(arg) === arg && !isDate && !isRegex) { // Object/function, not null/date/regexp. Use WeakMap to store the id first. // If it's already hashed, directly return the result. result = table.get(arg); if (result) return result; // Store the hash first for circular reference detection before entering the // recursive `stableHash` calls. // For other objects like set and map, we use this id directly as the hash. result = ++counter + '~'; table.set(arg, result); if (Array.isArray(arg)) { // Array. result = '@'; for(index = 0; index < arg.length; index++){ result += stableHash(arg[index]) + ','; } table.set(arg, result); } if (isPlainObject) { // Object, sort keys. result = '#'; const keys = OBJECT.keys(arg).sort(); while(!isUndefined(index = keys.pop())){ if (!isUndefined(arg[index])) { result += index + ':' + stableHash(arg[index]) + ','; } } table.set(arg, result); } } else { result = isDate ? arg.toJSON() : type == 'symbol' ? arg.toString() : type == 'string' ? JSON.stringify(arg) : '' + arg; } return result; }; const serialize = (key)=>{ if (isFunction(key)) { try { key = key(); } catch (err) { // dependencies not ready key = ''; } } // Use the original key as the argument of fetcher. This can be a string or an // array of values. const args = key; // If key is not falsy, or not an empty array, hash it. key = typeof key == 'string' ? key : (Array.isArray(key) ? key.length : key) ? stableHash(key) : ''; return [ key, args ]; }; // Global timestamp. let __timestamp = 0; const getTimestamp = ()=>++__timestamp; async function internalMutate(...args) { const [cache, _key, _data, _opts] = args; // When passing as a boolean, it's explicitly used to disable/enable // revalidation. const options = mergeObjects({ populateCache: true, throwOnError: true }, typeof _opts === 'boolean' ? { revalidate: _opts } : _opts || {}); let populateCache = options.populateCache; const rollbackOnErrorOption = options.rollbackOnError; let optimisticData = options.optimisticData; const rollbackOnError = (error)=>{ return typeof rollbackOnErrorOption === 'function' ? rollbackOnErrorOption(error) : rollbackOnErrorOption !== false; }; const throwOnError = options.throwOnError; // If the second argument is a key filter, return the mutation results for all // filtered keys. if (isFunction(_key)) { const keyFilter = _key; const matchedKeys = []; const it = cache.keys(); for (const key of it){ if (// Skip the special useSWRInfinite and useSWRSubscription keys. !/^\$(inf|sub)\$/.test(key) && keyFilter(cache.get(key)._k)) { matchedKeys.push(key); } } return Promise.all(matchedKeys.map(mutateByKey)); } return mutateByKey(_key); async function mutateByKey(_k) { // Serialize key const [key] = serialize(_k); if (!key) return; const [get, set] = createCacheHelper(cache, key); const [EVENT_REVALIDATORS, MUTATION, FETCH, PRELOAD] = SWRGlobalState.get(cache); const startRevalidate = ()=>{ const revalidators = EVENT_REVALIDATORS[key]; const revalidate = isFunction(options.revalidate) ? options.revalidate(get().data, _k) : options.revalidate !== false; if (revalidate) { // Invalidate the key by deleting the concurrent request markers so new // requests will not be deduped. delete FETCH[key]; delete PRELOAD[key]; if (revalidators && revalidators[0]) { return revalidators[0](revalidateEvents__namespace.MUTATE_EVENT).then(()=>get().data); } } return get().data; }; // If there is no new data provided, revalidate the key with current state. if (args.length < 3) { // Revalidate and broadcast state. return startRevalidate(); } let data = _data; let error; // Update global timestamps. const beforeMutationTs = getTimestamp(); MUTATION[key] = [ beforeMutationTs, 0 ]; const hasOptimisticData = !isUndefined(optimisticData); const state = get(); // `displayedData` is the current value on screen. It could be the optimistic value // that is going to be overridden by a `committedData`, or get reverted back. // `committedData` is the validated value that comes from a fetch or mutation. const displayedData = state.data; const currentData = state._c; const committedData = isUndefined(currentData) ? displayedData : currentData; // Do optimistic data update. if (hasOptimisticData) { optimisticData = isFunction(optimisticData) ? optimisticData(committedData, displayedData) : optimisticData; // When we set optimistic data, backup the current committedData data in `_c`. set({ data: optimisticData, _c: committedData }); } if (isFunction(data)) { // `data` is a function, call it passing current cache value. try { data = data(committedData); } catch (err) { // If it throws an error synchronously, we shouldn't update the cache. error = err; } } // `data` is a promise/thenable, resolve the final data first. if (data && isPromiseLike(data)) { // This means that the mutation is async, we need to check timestamps to // avoid race conditions. data = await data.catch((err)=>{ error = err; }); // Check if other mutations have occurred since we've started this mutation. // If there's a race we don't update cache or broadcast the change, // just return the data. if (beforeMutationTs !== MUTATION[key][0]) { if (error) throw error; return data; } else if (error && hasOptimisticData && rollbackOnError(error)) { // Rollback. Always populate the cache in this case but without // transforming the data. populateCache = true; // Reset data to be the latest committed data, and clear the `_c` value. set({ data: committedData, _c: UNDEFINED }); } } // If we should write back the cache after request. if (populateCache) { if (!error) { // Transform the result into data. if (isFunction(populateCache)) { const populateCachedData = populateCache(data, committedData); set({ data: populateCachedData, error: UNDEFINED, _c: UNDEFINED }); } else { // Only update cached data and reset the error if there's no error. Data can be `undefined` here. set({ data, error: UNDEFINED, _c: UNDEFINED }); } } } // Reset the timestamp to mark the mutation has ended. MUTATION[key][1] = getTimestamp(); // Update existing SWR Hooks' internal states: Promise.resolve(startRevalidate()).then(()=>{ // The mutation and revalidation are ended, we can clear it since the data is // not an optimistic value anymore. set({ _c: UNDEFINED }); }); // Throw error or return data if (error) { if (throwOnError) throw error; return; } return data; } } const revalidateAllKeys = (revalidators, type)=>{ for(const key in revalidators){ if (revalidators[key][0]) revalidators[key][0](type); } }; const initCache = (provider, options)=>{ // The global state for a specific provider will be used to deduplicate // requests and store listeners. As well as a mutate function that is bound to // the cache. // The provider's global state might be already initialized. Let's try to get the // global state associated with the provider first. if (!SWRGlobalState.has(provider)) { const opts = mergeObjects(defaultConfigOptions, options); // If there's no global state bound to the provider, create a new one with the // new mutate function. const EVENT_REVALIDATORS = Object.create(null); const mutate = internalMutate.bind(UNDEFINED, provider); let unmount = noop; const subscriptions = Object.create(null); const subscribe = (key, callback)=>{ const subs = subscriptions[key] || []; subscriptions[key] = subs; subs.push(callback); return ()=>subs.splice(subs.indexOf(callback), 1); }; const setter = (key, value, prev)=>{ provider.set(key, value); const subs = subscriptions[key]; if (subs) { for (const fn of subs){ fn(value, prev); } } }; const initProvider = ()=>{ if (!SWRGlobalState.has(provider)) { // Update the state if it's new, or if the provider has been extended. SWRGlobalState.set(provider, [ EVENT_REVALIDATORS, Object.create(null), Object.create(null), Object.create(null), mutate, setter, subscribe ]); if (!IS_SERVER) { // When listening to the native events for auto revalidations, // we intentionally put a delay (setTimeout) here to make sure they are // fired after immediate JavaScript executions, which can be // React's state updates. // This avoids some unnecessary revalidations such as // https://github.com/vercel/swr/issues/1680. const releaseFocus = opts.initFocus(setTimeout.bind(UNDEFINED, revalidateAllKeys.bind(UNDEFINED, EVENT_REVALIDATORS, revalidateEvents__namespace.FOCUS_EVENT))); const releaseReconnect = opts.initReconnect(setTimeout.bind(UNDEFINED, revalidateAllKeys.bind(UNDEFINED, EVENT_REVALIDATORS, revalidateEvents__namespace.RECONNECT_EVENT))); unmount = ()=>{ releaseFocus && releaseFocus(); releaseReconnect && releaseReconnect(); // When un-mounting, we need to remove the cache provider from the state // storage too because it's a side-effect. Otherwise, when re-mounting we // will not re-register those event listeners. SWRGlobalState.delete(provider); }; } } }; initProvider(); // This is a new provider, we need to initialize it and setup DOM events // listeners for `focus` and `reconnect` actions. // 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. return [ provider, mutate, initProvider, unmount ]; } return [ provider, SWRGlobalState.get(provider)[4] ]; }; // error retry const onErrorRetry = (_, __, config, revalidate, opts)=>{ const maxRetryCount = config.errorRetryCount; const currentRetryCount = opts.retryCount; // Exponential backoff const timeout = ~~((Math.random() + 0.5) * (1 << (currentRetryCount < 8 ? currentRetryCount : 8))) * config.errorRetryInterval; if (!isUndefined(maxRetryCount) && currentRetryCount > maxRetryCount) { return; } setTimeout(revalidate, timeout, opts); }; const compare = lite.dequal; // Default cache provider const [cache, mutate] = initCache(new Map()); // Default config const defaultConfig = mergeObjects({ // events onLoadingSlow: noop, onSuccess: noop, onError: noop, onErrorRetry, onDiscarded: noop, // switches revalidateOnFocus: true, revalidateOnReconnect: true, revalidateIfStale: true, shouldRetryOnError: true, // timeouts errorRetryInterval: slowConnection ? 10000 : 5000, focusThrottleInterval: 5 * 1000, dedupingInterval: 2 * 1000, loadingTimeout: slowConnection ? 5000 : 3000, // providers compare, isPaused: ()=>false, cache, mutate, fallback: {} }, // use web preset by default preset); const mergeConfigs = (a, b)=>{ // Need to create a new object to avoid mutating the original here. const v = mergeObjects(a, b); // If two configs are provided, merge their `use` and `fallback` options. if (b) { const { use: u1, fallback: f1 } = a; const { use: u2, fallback: f2 } = b; if (u1 && u2) { v.use = u1.concat(u2); } if (f1 && f2) { v.fallback = mergeObjects(f1, f2); } } return v; }; const SWRConfigContext = React.createContext({}); const SWRConfig = (props)=>{ const { value } = props; const parentConfig = React.useContext(SWRConfigContext); const isFunctionalConfig = isFunction(value); const config = React.useMemo(()=>isFunctionalConfig ? value(parentConfig) : value, [ isFunctionalConfig, parentConfig, value ]); // Extend parent context values and middleware. const extendedConfig = React.useMemo(()=>isFunctionalConfig ? config : mergeConfigs(parentConfig, config), [ isFunctionalConfig, parentConfig, config ]); // Should not use the inherited provider. const provider = config && config.provider; // initialize the cache only on first access. const cacheContextRef = React.useRef(UNDEFINED); if (provider && !cacheContextRef.current) { cacheContextRef.current = initCache(provider(extendedConfig.cache || cache), config); } const cacheContext = cacheContextRef.current; // Override the cache if a new provider is given. if (cacheContext) { extendedConfig.cache = cacheContext[0]; extendedConfig.mutate = cacheContext[1]; } // Unsubscribe events. useIsomorphicLayoutEffect(()=>{ if (cacheContext) { cacheContext[2] && cacheContext[2](); return cacheContext[3]; } }, []); return React.createElement(SWRConfigContext.Provider, mergeObjects(props, { value: extendedConfig })); }; exports.IS_REACT_LEGACY = IS_REACT_LEGACY; exports.IS_SERVER = IS_SERVER; exports.OBJECT = OBJECT; exports.SWRConfig = SWRConfig; exports.SWRConfigContext = SWRConfigContext; exports.SWRGlobalState = SWRGlobalState; exports.UNDEFINED = UNDEFINED; exports.cache = cache; exports.compare = compare; exports.createCacheHelper = createCacheHelper; exports.defaultConfig = defaultConfig; exports.defaultConfigOptions = defaultConfigOptions; exports.getTimestamp = getTimestamp; exports.hasRequestAnimationFrame = hasRequestAnimationFrame; exports.initCache = initCache; exports.internalMutate = internalMutate; exports.isDocumentDefined = isDocumentDefined; exports.isFunction = isFunction; exports.isLegacyDeno = isLegacyDeno; exports.isPromiseLike = isPromiseLike; exports.isUndefined = isUndefined; exports.isWindowDefined = isWindowDefined; exports.mergeConfigs = mergeConfigs; exports.mergeObjects = mergeObjects; exports.mutate = mutate; exports.noop = noop; exports.preset = preset; exports.rAF = rAF; exports.serialize = serialize; exports.slowConnection = slowConnection; exports.stableHash = stableHash; exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect;