react-async-hook
Version:
Async hook
316 lines (272 loc) • 9.99 kB
JavaScript
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
;