swr
Version:
React Hooks library for remote data fetching
616 lines (598 loc) • 23.5 kB
JavaScript
'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;