swr
Version:
React Hooks library for remote data fetching
768 lines (739 loc) • 28 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
var React = require('react');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
// Global state used to deduplicate requests and store listeners
const SWRGlobalState = new WeakMap();
const EMPTY_CACHE = {};
const INITIAL_CACHE = {};
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 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 hasRequestAnimationFrame = ()=>isWindowDefined && typeof window['requestAnimationFrame'] != STR_UNDEFINED;
const createCacheHelper = (cache, key)=>{
const state = SWRGlobalState.get(cache);
return [
// Getter
()=>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 cache.get(key) || EMPTY_CACHE;
}
];
};
// 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();
// 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 constructor = arg && arg.constructor;
const isDate = constructor == Date;
let result;
let index;
if (OBJECT(arg) === arg && !isDate && constructor != RegExp) {
// 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 (constructor == Array) {
// Array.
result = '@';
for(index = 0; index < arg.length; index++){
result += stableHash(arg[index]) + ',';
}
table.set(arg, result);
}
if (constructor == OBJECT) {
// 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;
};
/**
* 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 || 'Deno' in window;
// 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);
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;
const FOCUS_EVENT = 0;
const RECONNECT_EVENT = 1;
const MUTATE_EVENT = 2;
const ERROR_REVALIDATE_EVENT = 3;
var constants = {
__proto__: null,
FOCUS_EVENT: FOCUS_EVENT,
RECONNECT_EVENT: RECONNECT_EVENT,
MUTATE_EVENT: MUTATE_EVENT,
ERROR_REVALIDATE_EVENT: ERROR_REVALIDATE_EVENT
};
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 revalidate = options.revalidate !== false;
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(let keyIt = it.next(); !keyIt.done; keyIt = it.next()){
const key = keyIt.value;
if (// Skip the special useSWRInfinite keys.
!key.startsWith('$inf$') && 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] = SWRGlobalState.get(cache);
const revalidators = EVENT_REVALIDATORS[key];
const startRevalidate = ()=>{
if (revalidate) {
// Invalidate the key by deleting the concurrent request markers so new
// requests will not be deduped.
delete FETCH[key];
if (revalidators && revalidators[0]) {
return revalidators[0](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) : 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 && isFunction(data.then)) {
// 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;
data = committedData;
// Reset data to be the latest committed data, and clear the `_c` value.
set({
data,
_c: UNDEFINED
});
}
}
// If we should write back the cache after request.
if (populateCache) {
if (!error) {
// Transform the result into data.
if (isFunction(populateCache)) {
data = populateCache(data, committedData);
}
// Only update cached data if there's no error. Data can be `undefined` here.
set({
data,
_c: UNDEFINED
});
}
}
// Reset the timestamp to mark the mutation has ended.
MUTATION[key][1] = getTimestamp();
// Update existing SWR Hooks' internal states:
const res = await startRevalidate();
// 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 populateCache ? res : 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 = {};
const mutate = internalMutate.bind(UNDEFINED, provider);
let unmount = noop;
const subscriptions = {};
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,
{},
{},
{},
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, FOCUS_EVENT)));
const releaseReconnect = opts.initReconnect(setTimeout.bind(UNDEFINED, revalidateAllKeys.bind(UNDEFINED, EVENT_REVALIDATORS, 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 = (currentData, newData)=>stableHash(currentData) == stableHash(newData);
// 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;
// Use a lazy initialized state to create the cache on first access.
const [cacheContext] = React.useState(()=>provider ? initCache(provider(extendedConfig.cache || cache), config) : UNDEFINED);
// 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
}));
};
// @ts-expect-error
const enableDevtools = isWindowDefined && window.__SWR_DEVTOOLS_USE__;
const use = enableDevtools ? window.__SWR_DEVTOOLS_USE__ : [];
const setupDevTools = ()=>{
if (enableDevtools) {
// @ts-expect-error
window.__SWR_DEVTOOLS_REACT__ = React__default["default"];
}
};
const normalize = (args)=>{
return isFunction(args[1]) ? [
args[0],
args[1],
args[2] || {}
] : [
args[0],
null,
(args[1] === null ? args[2] : args[1]) || {}
];
};
const useSWRConfig = ()=>{
return mergeObjects(defaultConfig, React.useContext(SWRConfigContext));
};
const preload = (key_, fetcher)=>{
const key = serialize(key_)[0];
const [, , , PRELOAD] = SWRGlobalState.get(cache);
// Prevent preload to be called multiple times before used.
if (PRELOAD[key]) return PRELOAD[key];
const req = fetcher(key_);
PRELOAD[key] = req;
return req;
};
const middleware = (useSWRNext)=>(key_, fetcher_, config)=>{
// fetcher might be a sync function, so this should not be an async function
const fetcher = fetcher_ && ((...args)=>{
const key = serialize(key_)[0];
const [, , , PRELOAD] = SWRGlobalState.get(cache);
const req = PRELOAD[key];
if (req) {
delete PRELOAD[key];
return req;
}
return fetcher_(...args);
});
return useSWRNext(key_, fetcher, config);
};
const BUILT_IN_MIDDLEWARE = use.concat(middleware);
// It's tricky to pass generic types as parameters, so we just directly override
// the types here.
const withArgs = (hook)=>{
return function useSWRArgs(...args) {
// Get the default and inherited configuration.
const fallbackConfig = useSWRConfig();
// Normalize arguments.
const [key, fn, _config] = normalize(args);
// Merge configurations.
const config = mergeConfigs(fallbackConfig, _config);
// Apply middleware
let next = hook;
const { use } = config;
const middleware = (use || []).concat(BUILT_IN_MIDDLEWARE);
for(let i = middleware.length; i--;){
next = middleware[i](next);
}
return next(key, fn || config.fetcher || null, config);
};
};
/**
* An implementation of state with dependency-tracking.
*/ const useStateWithDeps = (state)=>{
const rerender = React.useState({})[1];
const unmountedRef = React.useRef(false);
const stateRef = React.useRef(state);
// If a state property (data, error, or isValidating) is accessed by the render
// function, we mark the property as a dependency so if it is updated again
// in the future, we trigger a rerender.
// This is also known as dependency-tracking.
const stateDependenciesRef = React.useRef({
data: false,
error: false,
isValidating: false
});
/**
* @param payload To change stateRef, pass the values explicitly to setState:
* @example
* ```js
* setState({
* isValidating: false
* data: newData // set data to newData
* error: undefined // set error to undefined
* })
*
* setState({
* isValidating: false
* data: undefined // set data to undefined
* error: err // set error to err
* })
* ```
*/ const setState = React.useCallback((payload)=>{
let shouldRerender = false;
const currentState = stateRef.current;
for(const _ in payload){
const k = _;
// If the property has changed, update the state and mark rerender as
// needed.
if (currentState[k] !== payload[k]) {
currentState[k] = payload[k];
// If the property is accessed by the component, a rerender should be
// triggered.
if (stateDependenciesRef.current[k]) {
shouldRerender = true;
}
}
}
if (shouldRerender && !unmountedRef.current) {
if (IS_REACT_LEGACY) {
rerender({});
} else {
React__default["default"].startTransition(()=>rerender({}));
}
}
}, // config.suspense isn't allowed to change during the lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
[]);
useIsomorphicLayoutEffect(()=>{
unmountedRef.current = false;
return ()=>{
unmountedRef.current = true;
};
});
return [
stateRef,
stateDependenciesRef.current,
setState
];
};
// Add a callback function to a list of keyed callback functions and return
// the unsubscribe function.
const subscribeCallback = (key, callbacks, callback)=>{
const keyedRevalidators = callbacks[key] || (callbacks[key] = []);
keyedRevalidators.push(callback);
return ()=>{
const index = keyedRevalidators.indexOf(callback);
if (index >= 0) {
// O(1): faster than splice
keyedRevalidators[index] = keyedRevalidators[keyedRevalidators.length - 1];
keyedRevalidators.pop();
}
};
};
// Create a custom hook with a middleware
const withMiddleware = (useSWR, middleware)=>{
return (...args)=>{
const [key, fn, config] = normalize(args);
const uses = (config.use || []).concat(middleware);
return useSWR(key, fn, {
...config,
use: uses
});
};
};
setupDevTools();
exports.IS_REACT_LEGACY = IS_REACT_LEGACY;
exports.IS_SERVER = IS_SERVER;
exports.OBJECT = OBJECT;
exports.SWRConfig = SWRConfig;
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.isUndefined = isUndefined;
exports.isWindowDefined = isWindowDefined;
exports.mergeConfigs = mergeConfigs;
exports.mergeObjects = mergeObjects;
exports.mutate = mutate;
exports.noop = noop;
exports.normalize = normalize;
exports.preload = preload;
exports.preset = preset;
exports.rAF = rAF;
exports.revalidateEvents = constants;
exports.serialize = serialize;
exports.slowConnection = slowConnection;
exports.stableHash = stableHash;
exports.subscribeCallback = subscribeCallback;
exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect;
exports.useSWRConfig = useSWRConfig;
exports.useStateWithDeps = useStateWithDeps;
exports.withArgs = withArgs;
exports.withMiddleware = withMiddleware;