react-async
Version:
React component for declarative promise resolution and data fetching
174 lines (147 loc) • 6.27 kB
JavaScript
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
import { useCallback, useDebugValue, useEffect, useMemo, useRef, useReducer } from "react";
import { actionTypes, init, reducer } from "./reducer.js";
const noop = () => {};
const useAsync = (arg1, arg2) => {
const counter = useRef(0);
const isMounted = useRef(true);
const lastArgs = useRef(undefined);
const prevOptions = useRef(undefined);
const abortController = useRef({
abort: noop
});
const options = typeof arg1 === "function" ? _objectSpread({}, arg2, {
promiseFn: arg1
}) : arg1;
const {
promise,
promiseFn,
deferFn,
initialValue,
onResolve,
onReject,
watch,
watchFn
} = options;
const [state, dispatch] = useReducer(reducer, options, init);
const setData = (data, callback = noop) => {
if (isMounted.current) {
dispatch({
type: actionTypes.fulfill,
payload: data
});
callback();
}
return data;
};
const setError = (error, callback = noop) => {
if (isMounted.current) {
dispatch({
type: actionTypes.reject,
payload: error,
error: true
});
callback();
}
return error;
};
const handleResolve = count => data => count === counter.current && setData(data, () => onResolve && onResolve(data));
const handleReject = count => error => count === counter.current && setError(error, () => onReject && onReject(error));
const start = () => {
if ("AbortController" in window) {
abortController.current.abort();
abortController.current = new window.AbortController();
}
counter.current++;
isMounted.current && dispatch({
type: actionTypes.start,
meta: {
counter: counter.current
}
});
};
const load = () => {
if (promise) {
start();
return promise.then(handleResolve(counter.current), handleReject(counter.current));
}
const isPreInitialized = initialValue && counter.current === 0;
if (promiseFn && !isPreInitialized) {
start();
return promiseFn(options, abortController.current).then(handleResolve(counter.current), handleReject(counter.current));
}
};
const run = (...args) => {
if (deferFn) {
lastArgs.current = args;
start();
return deferFn(args, options, abortController.current).then(handleResolve(counter.current), handleReject(counter.current));
}
};
const cancel = () => {
counter.current++;
abortController.current.abort();
isMounted.current && dispatch({
type: actionTypes.cancel,
meta: {
counter: counter.current
}
});
};
useEffect(() => {
if (watchFn && prevOptions.current && watchFn(options, prevOptions.current)) load();
});
useEffect(() => {
promise || promiseFn ? load() : cancel();
}, [promise, promiseFn, watch]);
useEffect(() => () => isMounted.current = false, []);
useEffect(() => () => abortController.current.abort(), []);
useEffect(() => (prevOptions.current = options) && undefined);
useDebugValue(state, ({
status
}) => `[${counter.current}] ${status}`);
return useMemo(() => _objectSpread({}, state, {
cancel,
run,
reload: () => lastArgs.current ? run(...lastArgs.current) : load(),
setData,
setError
}), [state, deferFn, onResolve, onReject]);
};
const parseResponse = (accept, json) => res => {
if (!res.ok) return Promise.reject(res);
if (json === false) return res;
if (json === true || accept === "application/json") return res.json();
return res;
};
const useAsyncFetch = (input, init, _ref = {}) => {
let {
defer,
json
} = _ref,
options = _objectWithoutProperties(_ref, ["defer", "json"]);
const method = input.method || init && init.method;
const headers = input.headers || init && init.headers || {};
const accept = headers["Accept"] || headers["accept"] || headers.get && headers.get("accept");
const doFetch = (input, init) => window.fetch(input, init).then(parseResponse(accept, json));
const isDefer = defer === true || ~["POST", "PUT", "PATCH", "DELETE"].indexOf(method);
const fn = defer === false || !isDefer ? "promiseFn" : "deferFn";
const state = useAsync(_objectSpread({}, options, {
[fn]: useCallback((_, props, ctrl) => doFetch(input, _objectSpread({
signal: ctrl ? ctrl.signal : props.signal
}, init)), [JSON.stringify(input), JSON.stringify(init)])
}));
useDebugValue(state, ({
counter,
status
}) => `[${counter}] ${status}`);
return state;
};
const unsupported = () => {
throw new Error("useAsync requires React v16.8 or up. Upgrade your React version or use the <Async> component instead.");
};
export default useEffect ? useAsync : unsupported;
export const useFetch = useEffect ? useAsyncFetch : unsupported;