react-query
Version:
Hooks for managing, caching and syncing asynchronous and remote data in React
523 lines (416 loc) • 16.7 kB
JavaScript
"use strict";
exports.__esModule = true;
exports.QueryObserver = void 0;
var _utils = require("./utils");
var _notifyManager = require("./notifyManager");
var _focusManager = require("./focusManager");
var _subscribable = require("./subscribable");
var _retryer = require("./retryer");
class QueryObserver extends _subscribable.Subscribable {
constructor(client, options) {
super();
this.client = client;
this.options = options;
this.trackedProps = new Set();
this.previousSelectError = null;
this.bindMethods();
this.setOptions(options);
}
bindMethods() {
this.remove = this.remove.bind(this);
this.refetch = this.refetch.bind(this);
}
onSubscribe() {
if (this.listeners.length === 1) {
this.currentQuery.addObserver(this);
if (shouldFetchOnMount(this.currentQuery, this.options)) {
this.executeFetch();
}
this.updateTimers();
}
}
onUnsubscribe() {
if (!this.listeners.length) {
this.destroy();
}
}
shouldFetchOnReconnect() {
return shouldFetchOn(this.currentQuery, this.options, this.options.refetchOnReconnect);
}
shouldFetchOnWindowFocus() {
return shouldFetchOn(this.currentQuery, this.options, this.options.refetchOnWindowFocus);
}
destroy() {
this.listeners = [];
this.clearStaleTimeout();
this.clearRefetchInterval();
this.currentQuery.removeObserver(this);
}
setOptions(options, notifyOptions) {
const prevOptions = this.options;
const prevQuery = this.currentQuery;
this.options = this.client.defaultQueryOptions(options);
if (typeof this.options.enabled !== 'undefined' && typeof this.options.enabled !== 'boolean') {
throw new Error('Expected enabled to be a boolean');
} // Keep previous query key if the user does not supply one
if (!this.options.queryKey) {
this.options.queryKey = prevOptions.queryKey;
}
this.updateQuery();
const mounted = this.hasListeners(); // Fetch if there are subscribers
if (mounted && shouldFetchOptionally(this.currentQuery, prevQuery, this.options, prevOptions)) {
this.executeFetch();
} // Update result
this.updateResult(notifyOptions); // Update stale interval if needed
if (mounted && (this.currentQuery !== prevQuery || this.options.enabled !== prevOptions.enabled || this.options.staleTime !== prevOptions.staleTime)) {
this.updateStaleTimeout();
}
const nextRefetchInterval = this.computeRefetchInterval(); // Update refetch interval if needed
if (mounted && (this.currentQuery !== prevQuery || this.options.enabled !== prevOptions.enabled || nextRefetchInterval !== this.currentRefetchInterval)) {
this.updateRefetchInterval(nextRefetchInterval);
}
}
getOptimisticResult(options) {
const query = this.client.getQueryCache().build(this.client, options);
return this.createResult(query, options);
}
getCurrentResult() {
return this.currentResult;
}
trackResult(result) {
const trackedResult = {};
Object.keys(result).forEach(key => {
Object.defineProperty(trackedResult, key, {
configurable: false,
enumerable: true,
get: () => {
this.trackedProps.add(key);
return result[key];
}
});
});
return trackedResult;
}
getCurrentQuery() {
return this.currentQuery;
}
remove() {
this.client.getQueryCache().remove(this.currentQuery);
}
refetch({
refetchPage,
...options
} = {}) {
return this.fetch({ ...options,
meta: {
refetchPage
}
});
}
fetchOptimistic(options) {
const defaultedOptions = this.client.defaultQueryOptions(options);
const query = this.client.getQueryCache().build(this.client, defaultedOptions);
query.isFetchingOptimistic = true;
return query.fetch().then(() => this.createResult(query, defaultedOptions));
}
fetch(fetchOptions) {
var _fetchOptions$cancelR;
return this.executeFetch({ ...fetchOptions,
cancelRefetch: (_fetchOptions$cancelR = fetchOptions.cancelRefetch) != null ? _fetchOptions$cancelR : true
}).then(() => {
this.updateResult();
return this.currentResult;
});
}
executeFetch(fetchOptions) {
// Make sure we reference the latest query as the current one might have been removed
this.updateQuery(); // Fetch
let promise = this.currentQuery.fetch(this.options, fetchOptions);
if (!(fetchOptions != null && fetchOptions.throwOnError)) {
promise = promise.catch(_utils.noop);
}
return promise;
}
updateStaleTimeout() {
this.clearStaleTimeout();
if (_utils.isServer || this.currentResult.isStale || !(0, _utils.isValidTimeout)(this.options.staleTime)) {
return;
}
const time = (0, _utils.timeUntilStale)(this.currentResult.dataUpdatedAt, this.options.staleTime); // The timeout is sometimes triggered 1 ms before the stale time expiration.
// To mitigate this issue we always add 1 ms to the timeout.
const timeout = time + 1;
this.staleTimeoutId = setTimeout(() => {
if (!this.currentResult.isStale) {
this.updateResult();
}
}, timeout);
}
computeRefetchInterval() {
var _this$options$refetch;
return typeof this.options.refetchInterval === 'function' ? this.options.refetchInterval(this.currentResult.data, this.currentQuery) : (_this$options$refetch = this.options.refetchInterval) != null ? _this$options$refetch : false;
}
updateRefetchInterval(nextInterval) {
this.clearRefetchInterval();
this.currentRefetchInterval = nextInterval;
if (_utils.isServer || this.options.enabled === false || !(0, _utils.isValidTimeout)(this.currentRefetchInterval) || this.currentRefetchInterval === 0) {
return;
}
this.refetchIntervalId = setInterval(() => {
if (this.options.refetchIntervalInBackground || _focusManager.focusManager.isFocused()) {
this.executeFetch();
}
}, this.currentRefetchInterval);
}
updateTimers() {
this.updateStaleTimeout();
this.updateRefetchInterval(this.computeRefetchInterval());
}
clearStaleTimeout() {
clearTimeout(this.staleTimeoutId);
this.staleTimeoutId = undefined;
}
clearRefetchInterval() {
clearInterval(this.refetchIntervalId);
this.refetchIntervalId = undefined;
}
createResult(query, options) {
const prevQuery = this.currentQuery;
const prevOptions = this.options;
const prevResult = this.currentResult;
const prevResultState = this.currentResultState;
const prevResultOptions = this.currentResultOptions;
const queryChange = query !== prevQuery;
const queryInitialState = queryChange ? query.state : this.currentQueryInitialState;
const prevQueryResult = queryChange ? this.currentResult : this.previousQueryResult;
const {
state
} = query;
let {
dataUpdatedAt,
error,
errorUpdatedAt,
fetchStatus,
status
} = state;
let isPreviousData = false;
let isPlaceholderData = false;
let data; // Optimistically set result in fetching state if needed
if (options._optimisticResults) {
const mounted = this.hasListeners();
const fetchOnMount = !mounted && shouldFetchOnMount(query, options);
const fetchOptionally = mounted && shouldFetchOptionally(query, prevQuery, options, prevOptions);
if (fetchOnMount || fetchOptionally) {
fetchStatus = (0, _retryer.canFetch)(query.options.networkMode) ? 'fetching' : 'paused';
if (!dataUpdatedAt) {
status = 'loading';
}
}
if (options._optimisticResults === 'isRestoring') {
fetchStatus = 'idle';
}
} // Keep previous data if needed
if (options.keepPreviousData && !state.dataUpdateCount && prevQueryResult != null && prevQueryResult.isSuccess && status !== 'error') {
data = prevQueryResult.data;
dataUpdatedAt = prevQueryResult.dataUpdatedAt;
status = prevQueryResult.status;
isPreviousData = true;
} // Select data if needed
else if (options.select && typeof state.data !== 'undefined') {
var _this$previousSelect;
// Memoize select result
if (prevResult && state.data === (prevResultState == null ? void 0 : prevResultState.data) && options.select === ((_this$previousSelect = this.previousSelect) == null ? void 0 : _this$previousSelect.fn) && !this.previousSelectError) {
data = this.previousSelect.result;
} else {
try {
data = options.select(state.data);
if (options.structuralSharing !== false) {
data = (0, _utils.replaceEqualDeep)(prevResult == null ? void 0 : prevResult.data, data);
}
this.previousSelect = {
fn: options.select,
result: data
};
this.previousSelectError = null;
} catch (selectError) {
if (process.env.NODE_ENV !== 'production') {
this.client.getLogger().error(selectError);
}
error = selectError;
this.previousSelectError = selectError;
errorUpdatedAt = Date.now();
status = 'error';
}
}
} // Use query data
else {
data = state.data;
} // Show placeholder data if needed
if (typeof options.placeholderData !== 'undefined' && typeof data === 'undefined' && status === 'loading') {
let placeholderData; // Memoize placeholder data
if (prevResult != null && prevResult.isPlaceholderData && options.placeholderData === (prevResultOptions == null ? void 0 : prevResultOptions.placeholderData)) {
placeholderData = prevResult.data;
} else {
placeholderData = typeof options.placeholderData === 'function' ? options.placeholderData() : options.placeholderData;
if (options.select && typeof placeholderData !== 'undefined') {
try {
placeholderData = options.select(placeholderData);
if (options.structuralSharing !== false) {
placeholderData = (0, _utils.replaceEqualDeep)(prevResult == null ? void 0 : prevResult.data, placeholderData);
}
this.previousSelectError = null;
} catch (selectError) {
if (process.env.NODE_ENV !== 'production') {
this.client.getLogger().error(selectError);
}
error = selectError;
this.previousSelectError = selectError;
errorUpdatedAt = Date.now();
status = 'error';
}
}
}
if (typeof placeholderData !== 'undefined') {
status = 'success';
data = placeholderData;
isPlaceholderData = true;
}
}
const isFetching = fetchStatus === 'fetching';
const result = {
status,
fetchStatus,
isLoading: status === 'loading',
isSuccess: status === 'success',
isError: status === 'error',
data,
dataUpdatedAt,
error,
errorUpdatedAt,
failureCount: state.fetchFailureCount,
isFetched: state.dataUpdateCount > 0 || state.errorUpdateCount > 0,
isFetchedAfterMount: state.dataUpdateCount > queryInitialState.dataUpdateCount || state.errorUpdateCount > queryInitialState.errorUpdateCount,
isFetching: isFetching,
isRefetching: isFetching && status !== 'loading',
isLoadingError: status === 'error' && state.dataUpdatedAt === 0,
isPaused: fetchStatus === 'paused',
isPlaceholderData,
isPreviousData,
isRefetchError: status === 'error' && state.dataUpdatedAt !== 0,
isStale: isStale(query, options),
refetch: this.refetch,
remove: this.remove
};
return result;
}
updateResult(notifyOptions) {
const prevResult = this.currentResult;
const nextResult = this.createResult(this.currentQuery, this.options);
this.currentResultState = this.currentQuery.state;
this.currentResultOptions = this.options; // Only notify and update result if something has changed
if ((0, _utils.shallowEqualObjects)(nextResult, prevResult)) {
return;
}
this.currentResult = nextResult; // Determine which callbacks to trigger
const defaultNotifyOptions = {
cache: true
};
const shouldNotifyListeners = () => {
if (!prevResult) {
return true;
}
const {
notifyOnChangeProps
} = this.options;
if (notifyOnChangeProps === 'all' || !notifyOnChangeProps && !this.trackedProps.size) {
return true;
}
const includedProps = new Set(notifyOnChangeProps != null ? notifyOnChangeProps : this.trackedProps);
if (this.options.useErrorBoundary) {
includedProps.add('error');
}
return Object.keys(this.currentResult).some(key => {
const typedKey = key;
const changed = this.currentResult[typedKey] !== prevResult[typedKey];
return changed && includedProps.has(typedKey);
});
};
if ((notifyOptions == null ? void 0 : notifyOptions.listeners) !== false && shouldNotifyListeners()) {
defaultNotifyOptions.listeners = true;
}
this.notify({ ...defaultNotifyOptions,
...notifyOptions
});
}
updateQuery() {
const query = this.client.getQueryCache().build(this.client, this.options);
if (query === this.currentQuery) {
return;
}
const prevQuery = this.currentQuery;
this.currentQuery = query;
this.currentQueryInitialState = query.state;
this.previousQueryResult = this.currentResult;
if (this.hasListeners()) {
prevQuery == null ? void 0 : prevQuery.removeObserver(this);
query.addObserver(this);
}
}
onQueryUpdate(action) {
const notifyOptions = {};
if (action.type === 'success') {
var _action$notifySuccess;
notifyOptions.onSuccess = (_action$notifySuccess = action.notifySuccess) != null ? _action$notifySuccess : true;
} else if (action.type === 'error' && !(0, _retryer.isCancelledError)(action.error)) {
notifyOptions.onError = true;
}
this.updateResult(notifyOptions);
if (this.hasListeners()) {
this.updateTimers();
}
}
notify(notifyOptions) {
_notifyManager.notifyManager.batch(() => {
// First trigger the configuration callbacks
if (notifyOptions.onSuccess) {
var _this$options$onSucce, _this$options, _this$options$onSettl, _this$options2;
(_this$options$onSucce = (_this$options = this.options).onSuccess) == null ? void 0 : _this$options$onSucce.call(_this$options, this.currentResult.data);
(_this$options$onSettl = (_this$options2 = this.options).onSettled) == null ? void 0 : _this$options$onSettl.call(_this$options2, this.currentResult.data, null);
} else if (notifyOptions.onError) {
var _this$options$onError, _this$options3, _this$options$onSettl2, _this$options4;
(_this$options$onError = (_this$options3 = this.options).onError) == null ? void 0 : _this$options$onError.call(_this$options3, this.currentResult.error);
(_this$options$onSettl2 = (_this$options4 = this.options).onSettled) == null ? void 0 : _this$options$onSettl2.call(_this$options4, undefined, this.currentResult.error);
} // Then trigger the listeners
if (notifyOptions.listeners) {
this.listeners.forEach(listener => {
listener(this.currentResult);
});
} // Then the cache listeners
if (notifyOptions.cache) {
this.client.getQueryCache().notify({
query: this.currentQuery,
type: 'observerResultsUpdated'
});
}
});
}
}
exports.QueryObserver = QueryObserver;
function shouldLoadOnMount(query, options) {
return options.enabled !== false && !query.state.dataUpdatedAt && !(query.state.status === 'error' && options.retryOnMount === false);
}
function shouldFetchOnMount(query, options) {
return shouldLoadOnMount(query, options) || query.state.dataUpdatedAt > 0 && shouldFetchOn(query, options, options.refetchOnMount);
}
function shouldFetchOn(query, options, field) {
if (options.enabled !== false) {
const value = typeof field === 'function' ? field(query) : field;
return value === 'always' || value !== false && isStale(query, options);
}
return false;
}
function shouldFetchOptionally(query, prevQuery, options, prevOptions) {
return options.enabled !== false && (query !== prevQuery || prevOptions.enabled === false) && (!options.suspense || query.state.status !== 'error') && isStale(query, options);
}
function isStale(query, options) {
return query.isStaleByTime(options.staleTime);
}