react-query
Version:
Hooks for managing, caching and syncing asynchronous and remote data in React
460 lines (371 loc) • 13.4 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import { functionalUpdate, isValidTimeout, noop, replaceEqualDeep, timeUntilStale, ensureQueryKeyArray } from './utils';
import { notifyManager } from './notifyManager';
import { getLogger } from './logger';
import { Retryer, isCancelledError } from './retryer'; // TYPES
// CLASS
export var Query = /*#__PURE__*/function () {
function Query(config) {
this.defaultOptions = config.defaultOptions;
this.setOptions(config.options);
this.observers = [];
this.cache = config.cache;
this.queryKey = config.queryKey;
this.queryHash = config.queryHash;
this.initialState = config.state || this.getDefaultState(this.options);
this.state = this.initialState;
this.scheduleGc();
}
var _proto = Query.prototype;
_proto.setOptions = function setOptions(options) {
var _this$options$cacheTi;
this.options = _extends({}, this.defaultOptions, options); // Default to 5 minutes if not cache time is set
this.cacheTime = Math.max(this.cacheTime || 0, (_this$options$cacheTi = this.options.cacheTime) != null ? _this$options$cacheTi : 5 * 60 * 1000);
};
_proto.setDefaultOptions = function setDefaultOptions(options) {
this.defaultOptions = options;
};
_proto.scheduleGc = function scheduleGc() {
var _this = this;
this.clearGcTimeout();
if (isValidTimeout(this.cacheTime)) {
this.gcTimeout = setTimeout(function () {
_this.optionalRemove();
}, this.cacheTime);
}
};
_proto.clearGcTimeout = function clearGcTimeout() {
clearTimeout(this.gcTimeout);
this.gcTimeout = undefined;
};
_proto.optionalRemove = function optionalRemove() {
if (!this.observers.length && !this.state.isFetching) {
this.cache.remove(this);
}
};
_proto.setData = function setData(updater, options) {
var _this$options$isDataE, _this$options;
var prevData = this.state.data; // Get the new data
var data = functionalUpdate(updater, prevData); // Use prev data if an isDataEqual function is defined and returns `true`
if ((_this$options$isDataE = (_this$options = this.options).isDataEqual) == null ? void 0 : _this$options$isDataE.call(_this$options, prevData, data)) {
data = prevData;
} else if (this.options.structuralSharing !== false) {
// Structurally share data between prev and new data if needed
data = replaceEqualDeep(prevData, data);
} // Set data and mark it as cached
this.dispatch({
data: data,
type: 'success',
dataUpdatedAt: options == null ? void 0 : options.updatedAt
});
return data;
};
_proto.setState = function setState(state, setStateOptions) {
this.dispatch({
type: 'setState',
state: state,
setStateOptions: setStateOptions
});
};
_proto.cancel = function cancel(options) {
var _this$retryer;
var promise = this.promise;
(_this$retryer = this.retryer) == null ? void 0 : _this$retryer.cancel(options);
return promise ? promise.then(noop).catch(noop) : Promise.resolve();
};
_proto.destroy = function destroy() {
this.clearGcTimeout();
this.cancel({
silent: true
});
};
_proto.reset = function reset() {
this.destroy();
this.setState(this.initialState);
};
_proto.isActive = function isActive() {
return this.observers.some(function (observer) {
return observer.options.enabled !== false;
});
};
_proto.isFetching = function isFetching() {
return this.state.isFetching;
};
_proto.isStale = function isStale() {
return this.state.isInvalidated || !this.state.dataUpdatedAt || this.observers.some(function (observer) {
return observer.getCurrentResult().isStale;
});
};
_proto.isStaleByTime = function isStaleByTime(staleTime) {
if (staleTime === void 0) {
staleTime = 0;
}
return this.state.isInvalidated || !this.state.dataUpdatedAt || !timeUntilStale(this.state.dataUpdatedAt, staleTime);
};
_proto.onFocus = function onFocus() {
var _this$retryer2;
var observer = this.observers.find(function (x) {
return x.shouldFetchOnWindowFocus();
});
if (observer) {
observer.refetch();
} // Continue fetch if currently paused
(_this$retryer2 = this.retryer) == null ? void 0 : _this$retryer2.continue();
};
_proto.onOnline = function onOnline() {
var _this$retryer3;
var observer = this.observers.find(function (x) {
return x.shouldFetchOnReconnect();
});
if (observer) {
observer.refetch();
} // Continue fetch if currently paused
(_this$retryer3 = this.retryer) == null ? void 0 : _this$retryer3.continue();
};
_proto.addObserver = function addObserver(observer) {
if (this.observers.indexOf(observer) === -1) {
this.observers.push(observer); // Stop the query from being garbage collected
this.clearGcTimeout();
this.cache.notify({
type: 'observerAdded',
query: this,
observer: observer
});
}
};
_proto.removeObserver = function removeObserver(observer) {
if (this.observers.indexOf(observer) !== -1) {
this.observers = this.observers.filter(function (x) {
return x !== observer;
});
if (!this.observers.length) {
// If the transport layer does not support cancellation
// we'll let the query continue so the result can be cached
if (this.retryer) {
if (this.retryer.isTransportCancelable) {
this.retryer.cancel({
revert: true
});
} else {
this.retryer.cancelRetry();
}
}
if (this.cacheTime) {
this.scheduleGc();
} else {
this.cache.remove(this);
}
}
this.cache.notify({
type: 'observerRemoved',
query: this,
observer: observer
});
}
};
_proto.getObserversCount = function getObserversCount() {
return this.observers.length;
};
_proto.invalidate = function invalidate() {
if (!this.state.isInvalidated) {
this.dispatch({
type: 'invalidate'
});
}
};
_proto.fetch = function fetch(options, fetchOptions) {
var _this2 = this,
_this$options$behavio,
_context$fetchOptions;
if (this.state.isFetching) {
if (this.state.dataUpdatedAt && (fetchOptions == null ? void 0 : fetchOptions.cancelRefetch)) {
// Silently cancel current fetch if the user wants to cancel refetches
this.cancel({
silent: true
});
} else if (this.promise) {
// Return current promise if we are already fetching
return this.promise;
}
} // Update config if passed, otherwise the config from the last execution is used
if (options) {
this.setOptions(options);
} // Use the options from the first observer with a query function if no function is found.
// This can happen when the query is hydrated or created with setQueryData.
if (!this.options.queryFn) {
var observer = this.observers.find(function (x) {
return x.options.queryFn;
});
if (observer) {
this.setOptions(observer.options);
}
}
var queryKey = ensureQueryKeyArray(this.queryKey); // Create query function context
var queryFnContext = {
queryKey: queryKey,
pageParam: undefined
}; // Create fetch function
var fetchFn = function fetchFn() {
return _this2.options.queryFn ? _this2.options.queryFn(queryFnContext) : Promise.reject('Missing queryFn');
}; // Trigger behavior hook
var context = {
fetchOptions: fetchOptions,
options: this.options,
queryKey: queryKey,
state: this.state,
fetchFn: fetchFn
};
if ((_this$options$behavio = this.options.behavior) == null ? void 0 : _this$options$behavio.onFetch) {
var _this$options$behavio2;
(_this$options$behavio2 = this.options.behavior) == null ? void 0 : _this$options$behavio2.onFetch(context);
} // Store state in case the current fetch needs to be reverted
this.revertState = this.state; // Set to fetching state if not already in it
if (!this.state.isFetching || this.state.fetchMeta !== ((_context$fetchOptions = context.fetchOptions) == null ? void 0 : _context$fetchOptions.meta)) {
var _context$fetchOptions2;
this.dispatch({
type: 'fetch',
meta: (_context$fetchOptions2 = context.fetchOptions) == null ? void 0 : _context$fetchOptions2.meta
});
} // Try to fetch the data
this.retryer = new Retryer({
fn: context.fetchFn,
onSuccess: function onSuccess(data) {
_this2.setData(data); // Notify cache callback
_this2.cache.config.onSuccess == null ? void 0 : _this2.cache.config.onSuccess(data, _this2); // Remove query after fetching if cache time is 0
if (_this2.cacheTime === 0) {
_this2.optionalRemove();
}
},
onError: function onError(error) {
// Optimistically update state if needed
if (!(isCancelledError(error) && error.silent)) {
_this2.dispatch({
type: 'error',
error: error
});
}
if (!isCancelledError(error)) {
// Notify cache callback
_this2.cache.config.onError == null ? void 0 : _this2.cache.config.onError(error, _this2); // Log error
getLogger().error(error);
} // Remove query after fetching if cache time is 0
if (_this2.cacheTime === 0) {
_this2.optionalRemove();
}
},
onFail: function onFail() {
_this2.dispatch({
type: 'failed'
});
},
onPause: function onPause() {
_this2.dispatch({
type: 'pause'
});
},
onContinue: function onContinue() {
_this2.dispatch({
type: 'continue'
});
},
retry: context.options.retry,
retryDelay: context.options.retryDelay
});
this.promise = this.retryer.promise;
return this.promise;
};
_proto.dispatch = function dispatch(action) {
var _this3 = this;
this.state = this.reducer(this.state, action);
notifyManager.batch(function () {
_this3.observers.forEach(function (observer) {
observer.onQueryUpdate(action);
});
_this3.cache.notify({
query: _this3,
type: 'queryUpdated',
action: action
});
});
};
_proto.getDefaultState = function getDefaultState(options) {
var data = typeof options.initialData === 'function' ? options.initialData() : options.initialData;
var hasInitialData = typeof options.initialData !== 'undefined';
var initialDataUpdatedAt = hasInitialData ? typeof options.initialDataUpdatedAt === 'function' ? options.initialDataUpdatedAt() : options.initialDataUpdatedAt : 0;
var hasData = typeof data !== 'undefined';
return {
data: data,
dataUpdateCount: 0,
dataUpdatedAt: hasData ? initialDataUpdatedAt != null ? initialDataUpdatedAt : Date.now() : 0,
error: null,
errorUpdateCount: 0,
errorUpdatedAt: 0,
fetchFailureCount: 0,
fetchMeta: null,
isFetching: false,
isInvalidated: false,
isPaused: false,
status: hasData ? 'success' : 'idle'
};
};
_proto.reducer = function reducer(state, action) {
var _action$meta, _action$dataUpdatedAt;
switch (action.type) {
case 'failed':
return _extends({}, state, {
fetchFailureCount: state.fetchFailureCount + 1
});
case 'pause':
return _extends({}, state, {
isPaused: true
});
case 'continue':
return _extends({}, state, {
isPaused: false
});
case 'fetch':
return _extends({}, state, {
fetchFailureCount: 0,
fetchMeta: (_action$meta = action.meta) != null ? _action$meta : null,
isFetching: true,
isPaused: false,
status: !state.dataUpdatedAt ? 'loading' : state.status
});
case 'success':
return _extends({}, state, {
data: action.data,
dataUpdateCount: state.dataUpdateCount + 1,
dataUpdatedAt: (_action$dataUpdatedAt = action.dataUpdatedAt) != null ? _action$dataUpdatedAt : Date.now(),
error: null,
fetchFailureCount: 0,
isFetching: false,
isInvalidated: false,
isPaused: false,
status: 'success'
});
case 'error':
var error = action.error;
if (isCancelledError(error) && error.revert && this.revertState) {
return _extends({}, this.revertState);
}
return _extends({}, state, {
error: error,
errorUpdateCount: state.errorUpdateCount + 1,
errorUpdatedAt: Date.now(),
fetchFailureCount: state.fetchFailureCount + 1,
isFetching: false,
isPaused: false,
status: 'error'
});
case 'invalidate':
return _extends({}, state, {
isInvalidated: true
});
case 'setState':
return _extends({}, state, action.state);
default:
return state;
}
};
return Query;
}();