UNPKG

react-async-hook

Version:
316 lines (272 loc) 9.99 kB
'use strict'; var react = require('react'); function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } // A type of promise-like that resolves synchronously and supports only one observer const _iteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.iterator || (Symbol.iterator = Symbol("Symbol.iterator"))) : "@@iterator"; const _asyncIteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.asyncIterator || (Symbol.asyncIterator = Symbol("Symbol.asyncIterator"))) : "@@asyncIterator"; // Asynchronously await a promise and pass the result to a finally continuation function _finallyRethrows(body, finalizer) { try { var result = body(); } catch (e) { return finalizer(true, e); } if (result && result.then) { return result.then(finalizer.bind(null, false), finalizer.bind(null, true)); } return finalizer(false, value); } var useIsomorphicLayoutEffect = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined' ? react.useLayoutEffect : react.useEffect; // Assign current value to a ref and returns a stable getter to get the latest value. // This way we are sure to always get latest value provided to hook and // avoid weird issues due to closures capturing stale values... // See https://github.com/facebook/react/issues/16956 // See https://overreacted.io/making-setinterval-declarative-with-react-hooks/ var useGetter = function useGetter(t) { var ref = react.useRef(t); useIsomorphicLayoutEffect(function () { ref.current = t; }); return react.useCallback(function () { return ref.current; }, [ref]); }; var InitialAsyncState = { status: 'not-requested', loading: false, result: undefined, error: undefined }; var InitialAsyncLoadingState = { status: 'loading', loading: true, result: undefined, error: undefined }; var defaultSetLoading = function defaultSetLoading(_asyncState) { return InitialAsyncLoadingState; }; var defaultSetResult = function defaultSetResult(result, _asyncState) { return { status: 'success', loading: false, result: result, error: undefined }; }; var defaultSetError = function defaultSetError(error, _asyncState) { return { status: 'error', loading: false, result: undefined, error: error }; }; var noop = function noop() {}; var DefaultOptions = { initialState: function initialState(options) { return options && options.executeOnMount ? InitialAsyncLoadingState : InitialAsyncState; }, executeOnMount: true, executeOnUpdate: true, setLoading: defaultSetLoading, setResult: defaultSetResult, setError: defaultSetError, onSuccess: noop, onError: noop }; var normalizeOptions = function normalizeOptions(options) { return _extends({}, DefaultOptions, {}, options); }; var useAsyncState = function useAsyncState(options) { var _useState = react.useState(function () { return options.initialState(options); }), value = _useState[0], setValue = _useState[1]; var reset = react.useCallback(function () { return setValue(options.initialState(options)); }, [setValue, options]); var setLoading = react.useCallback(function () { return setValue(options.setLoading(value)); }, [value, setValue]); var setResult = react.useCallback(function (result) { return setValue(options.setResult(result, value)); }, [value, setValue]); var setError = react.useCallback(function (error) { return setValue(options.setError(error, value)); }, [value, setValue]); var merge = react.useCallback(function (state) { return setValue(_extends({}, value, {}, state)); }, [value, setValue]); return { value: value, set: setValue, merge: merge, reset: reset, setLoading: setLoading, setResult: setResult, setError: setError }; }; var useIsMounted = function useIsMounted() { var ref = react.useRef(false); react.useEffect(function () { ref.current = true; return function () { ref.current = false; }; }, []); return function () { return ref.current; }; }; var useCurrentPromise = function useCurrentPromise() { var ref = react.useRef(null); return { set: function set(promise) { return ref.current = promise; }, get: function get() { return ref.current; }, is: function is(promise) { return ref.current === promise; } }; }; // Relaxed interface which accept both async and sync functions // Accepting sync function is convenient for useAsyncCallback var useAsyncInternal = function useAsyncInternal(asyncFunction, params, options) { // Fallback missing params, only for JS users forgetting the deps array, to prevent infinite loops // https://github.com/slorber/react-async-hook/issues/27 // @ts-ignore !params && (params = []); var normalizedOptions = normalizeOptions(options); var _useState2 = react.useState(null), currentParams = _useState2[0], setCurrentParams = _useState2[1]; var AsyncState = useAsyncState(normalizedOptions); var isMounted = useIsMounted(); var CurrentPromise = useCurrentPromise(); // We only want to handle the promise result/error // if it is the last operation and the comp is still mounted var shouldHandlePromise = function shouldHandlePromise(p) { return isMounted() && CurrentPromise.is(p); }; var executeAsyncOperation = function executeAsyncOperation() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } // async ensures errors thrown synchronously are caught (ie, bug when formatting api payloads) // async ensures promise-like and synchronous functions are handled correctly too // see https://github.com/slorber/react-async-hook/issues/24 var promise = function () { try { return Promise.resolve(asyncFunction.apply(void 0, args)); } catch (e) { return Promise.reject(e); } }(); setCurrentParams(args); CurrentPromise.set(promise); AsyncState.setLoading(); promise.then(function (result) { if (shouldHandlePromise(promise)) { AsyncState.setResult(result); } normalizedOptions.onSuccess(result, { isCurrent: function isCurrent() { return CurrentPromise.is(promise); } }); }, function (error) { if (shouldHandlePromise(promise)) { AsyncState.setError(error); } normalizedOptions.onError(error, { isCurrent: function isCurrent() { return CurrentPromise.is(promise); } }); }); return promise; }; var getLatestExecuteAsyncOperation = useGetter(executeAsyncOperation); var executeAsyncOperationMemo = react.useCallback(function () { return getLatestExecuteAsyncOperation().apply(void 0, arguments); }, [getLatestExecuteAsyncOperation]); // Keep this outside useEffect, because inside isMounted() // will be true as the component is already mounted when it's run var isMounting = !isMounted(); react.useEffect(function () { var execute = function execute() { return getLatestExecuteAsyncOperation().apply(void 0, params); }; isMounting && normalizedOptions.executeOnMount && execute(); !isMounting && normalizedOptions.executeOnUpdate && execute(); }, params); return _extends({}, AsyncState.value, { set: AsyncState.set, merge: AsyncState.merge, reset: AsyncState.reset, execute: executeAsyncOperationMemo, currentPromise: CurrentPromise.get(), currentParams: currentParams }); }; function useAsync(asyncFunction, params, options) { return useAsyncInternal(asyncFunction, params, options); } var useAsyncAbortable = function useAsyncAbortable(asyncFunction, params, options) { var abortControllerRef = react.useRef(); // Wrap the original async function and enhance it with abortion login var asyncFunctionWrapper = function asyncFunctionWrapper() { for (var _len2 = arguments.length, p = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { p[_key2] = arguments[_key2]; } try { // Cancel previous async call if (abortControllerRef.current) { abortControllerRef.current.abort(); } // Create/store new abort controller for next async call var abortController = new AbortController(); abortControllerRef.current = abortController; return Promise.resolve(_finallyRethrows(function () { // @ts-ignore // TODO how to type this? return Promise.resolve(asyncFunction.apply(void 0, [abortController.signal].concat(p))); }, function (_wasThrown, _result) { // Unset abortController ref if response is already there, // as it's not needed anymore to try to abort it (would it be no-op?) if (abortControllerRef.current === abortController) { abortControllerRef.current = undefined; } if (_wasThrown) throw _result; return _result; })); } catch (e) { return Promise.reject(e); } }; return useAsync(asyncFunctionWrapper, params, options); }; var useAsyncCallback = function useAsyncCallback(asyncFunction, options) { return useAsyncInternal(asyncFunction, // Hacky but in such case we don't need the params, // because async function is only executed manually [], _extends({}, options, { executeOnMount: false, executeOnUpdate: false })); }; exports.useAsync = useAsync; exports.useAsyncAbortable = useAsyncAbortable; exports.useAsyncCallback = useAsyncCallback; //# sourceMappingURL=react-async-hook.cjs.development.js.map