UNPKG

react-query

Version:

Hooks for managing, caching and syncing asynchronous and remote data in React

477 lines (385 loc) 13.4 kB
"use strict"; exports.__esModule = true; exports.Query = void 0; var _utils = require("./utils"); var _logger = require("./logger"); var _notifyManager = require("./notifyManager"); var _retryer = require("./retryer"); var _removable = require("./removable"); // CLASS class Query extends _removable.Removable { constructor(config) { super(); this.abortSignalConsumed = false; this.defaultOptions = config.defaultOptions; this.setOptions(config.options); this.observers = []; this.cache = config.cache; this.logger = config.logger || _logger.defaultLogger; this.queryKey = config.queryKey; this.queryHash = config.queryHash; this.initialState = config.state || getDefaultState(this.options); this.state = this.initialState; this.meta = config.meta; } setOptions(options) { this.options = { ...this.defaultOptions, ...options }; this.meta = options == null ? void 0 : options.meta; this.updateCacheTime(this.options.cacheTime); } optionalRemove() { if (!this.observers.length && this.state.fetchStatus === 'idle') { this.cache.remove(this); } } setData(data, options) { var _this$options$isDataE, _this$options; const prevData = this.state.data; // Use prev data if an isDataEqual function is defined and returns `true` if ((_this$options$isDataE = (_this$options = this.options).isDataEqual) != null && _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 = (0, _utils.replaceEqualDeep)(prevData, data); } // Set data and mark it as cached this.dispatch({ data, type: 'success', dataUpdatedAt: options == null ? void 0 : options.updatedAt, notifySuccess: options == null ? void 0 : options.notifySuccess }); return data; } setState(state, setStateOptions) { this.dispatch({ type: 'setState', state, setStateOptions }); } cancel(options) { var _this$retryer; const promise = this.promise; (_this$retryer = this.retryer) == null ? void 0 : _this$retryer.cancel(options); return promise ? promise.then(_utils.noop).catch(_utils.noop) : Promise.resolve(); } destroy() { super.destroy(); this.cancel({ silent: true }); } reset() { this.destroy(); this.setState(this.initialState); } isActive() { return this.observers.some(observer => observer.options.enabled !== false); } isDisabled() { return this.getObserversCount() > 0 && !this.isActive(); } isStale() { return this.state.isInvalidated || !this.state.dataUpdatedAt || this.observers.some(observer => observer.getCurrentResult().isStale); } isStaleByTime(staleTime = 0) { return this.state.isInvalidated || !this.state.dataUpdatedAt || !(0, _utils.timeUntilStale)(this.state.dataUpdatedAt, staleTime); } onFocus() { var _this$retryer2; const observer = this.observers.find(x => x.shouldFetchOnWindowFocus()); if (observer) { observer.refetch({ cancelRefetch: false }); } // Continue fetch if currently paused (_this$retryer2 = this.retryer) == null ? void 0 : _this$retryer2.continue(); } onOnline() { var _this$retryer3; const observer = this.observers.find(x => x.shouldFetchOnReconnect()); if (observer) { observer.refetch({ cancelRefetch: false }); } // Continue fetch if currently paused (_this$retryer3 = this.retryer) == null ? void 0 : _this$retryer3.continue(); } 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 }); } } removeObserver(observer) { if (this.observers.indexOf(observer) !== -1) { this.observers = this.observers.filter(x => 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.abortSignalConsumed) { this.retryer.cancel({ revert: true }); } else { this.retryer.cancelRetry(); } } this.scheduleGc(); } this.cache.notify({ type: 'observerRemoved', query: this, observer }); } } getObserversCount() { return this.observers.length; } invalidate() { if (!this.state.isInvalidated) { this.dispatch({ type: 'invalidate' }); } } fetch(options, fetchOptions) { var _this$options$behavio, _context$fetchOptions; if (this.state.fetchStatus !== 'idle') { if (this.state.dataUpdatedAt && fetchOptions != null && fetchOptions.cancelRefetch) { // Silently cancel current fetch if the user wants to cancel refetches this.cancel({ silent: true }); } else if (this.promise) { var _this$retryer4; // make sure that retries that were potentially cancelled due to unmounts can continue (_this$retryer4 = this.retryer) == null ? void 0 : _this$retryer4.continueRetry(); // 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) { const observer = this.observers.find(x => x.options.queryFn); if (observer) { this.setOptions(observer.options); } } if (!Array.isArray(this.options.queryKey)) { if (process.env.NODE_ENV !== 'production') { this.logger.error("As of v4, queryKey needs to be an Array. If you are using a string like 'repoData', please change it to an Array, e.g. ['repoData']"); } } const abortController = (0, _utils.getAbortController)(); // Create query function context const queryFnContext = { queryKey: this.queryKey, pageParam: undefined, meta: this.meta }; // Adds an enumerable signal property to the object that // which sets abortSignalConsumed to true when the signal // is read. const addSignalProperty = object => { Object.defineProperty(object, 'signal', { enumerable: true, get: () => { if (abortController) { this.abortSignalConsumed = true; return abortController.signal; } return undefined; } }); }; addSignalProperty(queryFnContext); // Create fetch function const fetchFn = () => { if (!this.options.queryFn) { return Promise.reject('Missing queryFn'); } this.abortSignalConsumed = false; return this.options.queryFn(queryFnContext); }; // Trigger behavior hook const context = { fetchOptions, options: this.options, queryKey: this.queryKey, state: this.state, fetchFn, meta: this.meta }; addSignalProperty(context); (_this$options$behavio = this.options.behavior) == null ? void 0 : _this$options$behavio.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.fetchStatus === 'idle' || 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 }); } const onError = error => { // Optimistically update state if needed if (!((0, _retryer.isCancelledError)(error) && error.silent)) { this.dispatch({ type: 'error', error: error }); } if (!(0, _retryer.isCancelledError)(error)) { var _this$cache$config$on, _this$cache$config; // Notify cache callback (_this$cache$config$on = (_this$cache$config = this.cache.config).onError) == null ? void 0 : _this$cache$config$on.call(_this$cache$config, error, this); if (process.env.NODE_ENV !== 'production') { this.logger.error(error); } } if (!this.isFetchingOptimistic) { // Schedule query gc after fetching this.scheduleGc(); } this.isFetchingOptimistic = false; }; // Try to fetch the data this.retryer = (0, _retryer.createRetryer)({ fn: context.fetchFn, abort: abortController == null ? void 0 : abortController.abort.bind(abortController), onSuccess: data => { var _this$cache$config$on2, _this$cache$config2; if (typeof data === 'undefined') { onError(new Error('Query data cannot be undefined')); return; } this.setData(data); // Notify cache callback (_this$cache$config$on2 = (_this$cache$config2 = this.cache.config).onSuccess) == null ? void 0 : _this$cache$config$on2.call(_this$cache$config2, data, this); if (!this.isFetchingOptimistic) { // Schedule query gc after fetching this.scheduleGc(); } this.isFetchingOptimistic = false; }, onError, onFail: () => { this.dispatch({ type: 'failed' }); }, onPause: () => { this.dispatch({ type: 'pause' }); }, onContinue: () => { this.dispatch({ type: 'continue' }); }, retry: context.options.retry, retryDelay: context.options.retryDelay, networkMode: context.options.networkMode }); this.promise = this.retryer.promise; return this.promise; } dispatch(action) { const reducer = state => { var _action$meta, _action$dataUpdatedAt; switch (action.type) { case 'failed': return { ...state, fetchFailureCount: state.fetchFailureCount + 1 }; case 'pause': return { ...state, fetchStatus: 'paused' }; case 'continue': return { ...state, fetchStatus: 'fetching' }; case 'fetch': return { ...state, fetchFailureCount: 0, fetchMeta: (_action$meta = action.meta) != null ? _action$meta : null, fetchStatus: (0, _retryer.canFetch)(this.options.networkMode) ? 'fetching' : 'paused', ...(!state.dataUpdatedAt && { error: null, status: 'loading' }) }; case 'success': return { ...state, data: action.data, dataUpdateCount: state.dataUpdateCount + 1, dataUpdatedAt: (_action$dataUpdatedAt = action.dataUpdatedAt) != null ? _action$dataUpdatedAt : Date.now(), error: null, fetchFailureCount: 0, isInvalidated: false, fetchStatus: 'idle', status: 'success' }; case 'error': const error = action.error; if ((0, _retryer.isCancelledError)(error) && error.revert && this.revertState) { return { ...this.revertState }; } return { ...state, error: error, errorUpdateCount: state.errorUpdateCount + 1, errorUpdatedAt: Date.now(), fetchFailureCount: state.fetchFailureCount + 1, fetchStatus: 'idle', status: 'error' }; case 'invalidate': return { ...state, isInvalidated: true }; case 'setState': return { ...state, ...action.state }; } }; this.state = reducer(this.state); _notifyManager.notifyManager.batch(() => { this.observers.forEach(observer => { observer.onQueryUpdate(action); }); this.cache.notify({ query: this, type: 'updated', action }); }); } } exports.Query = Query; function getDefaultState(options) { const data = typeof options.initialData === 'function' ? options.initialData() : options.initialData; const hasInitialData = typeof options.initialData !== 'undefined'; const initialDataUpdatedAt = hasInitialData ? typeof options.initialDataUpdatedAt === 'function' ? options.initialDataUpdatedAt() : options.initialDataUpdatedAt : 0; const hasData = typeof data !== 'undefined'; return { data, dataUpdateCount: 0, dataUpdatedAt: hasData ? initialDataUpdatedAt != null ? initialDataUpdatedAt : Date.now() : 0, error: null, errorUpdateCount: 0, errorUpdatedAt: 0, fetchFailureCount: 0, fetchMeta: null, isInvalidated: false, status: hasData ? 'success' : 'loading', fetchStatus: 'idle' }; }