react-async
Version:
React component for declarative promise resolution and data fetching
771 lines (652 loc) • 21.5 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var React = require('react');
var React__default = _interopDefault(React);
const statusTypes = {
initial: "initial",
pending: "pending",
fulfilled: "fulfilled",
rejected: "rejected"
};
const getInitialStatus = (value, promise) => {
if (value instanceof Error) return statusTypes.rejected;
if (value !== undefined) return statusTypes.fulfilled;
if (promise) return statusTypes.pending;
return statusTypes.initial;
};
const getIdleStatus = value => {
if (value instanceof Error) return statusTypes.rejected;
if (value !== undefined) return statusTypes.fulfilled;
return statusTypes.initial;
};
const getStatusProps = status => ({
status,
isInitial: status === statusTypes.initial,
isPending: status === statusTypes.pending,
isLoading: status === statusTypes.pending,
// alias
isFulfilled: status === statusTypes.fulfilled,
isResolved: status === statusTypes.fulfilled,
// alias
isRejected: status === statusTypes.rejected,
isSettled: status === statusTypes.fulfilled || status === statusTypes.rejected
});
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;
}
const actionTypes = {
start: "start",
cancel: "cancel",
fulfill: "fulfill",
reject: "reject"
};
const init = ({
initialValue,
promise,
promiseFn
}) => _objectSpread({
initialValue,
data: initialValue instanceof Error ? undefined : initialValue,
error: initialValue instanceof Error ? initialValue : undefined,
value: initialValue,
startedAt: promise || promiseFn ? new Date() : undefined,
finishedAt: initialValue ? new Date() : undefined
}, getStatusProps(getInitialStatus(initialValue, promise || promiseFn)), {
counter: 0
});
const reducer = (state, {
type,
payload,
meta
}) => {
switch (type) {
case actionTypes.start:
return _objectSpread({}, state, {
startedAt: new Date(),
finishedAt: undefined
}, getStatusProps(statusTypes.pending), {
counter: meta.counter
});
case actionTypes.cancel:
return _objectSpread({}, state, {
startedAt: undefined,
finishedAt: undefined
}, getStatusProps(getIdleStatus(state.error || state.data)), {
counter: meta.counter
});
case actionTypes.fulfill:
return _objectSpread({}, state, {
data: payload,
value: payload,
error: undefined,
finishedAt: new Date()
}, getStatusProps(statusTypes.fulfilled));
case actionTypes.reject:
return _objectSpread({}, state, {
error: payload,
value: payload,
finishedAt: new Date()
}, getStatusProps(statusTypes.rejected));
}
};
function _objectSpread$1(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$1(target, key, source[key]);
});
}
return target;
}
function _defineProperty$1(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;
}
let PropTypes;
try {
PropTypes = require("prop-types");
} catch (e) {}
const isFunction = arg => typeof arg === "function";
const renderFn = (children, ...args) => isFunction(children) ? children(...args) : children === undefined ? null : children;
/**
* createInstance allows you to create instances of Async that are bound to a specific promise.
* A unique instance also uses its own React context for better nesting capability.
*/
const createInstance = (defaultProps = {}, displayName = "Async") => {
const _React$createContext = React__default.createContext(),
Consumer = _React$createContext.Consumer,
Provider = _React$createContext.Provider;
class Async extends React__default.Component {
constructor(props) {
super(props);
this.start = this.start.bind(this);
this.load = this.load.bind(this);
this.run = this.run.bind(this);
this.cancel = this.cancel.bind(this);
this.onResolve = this.onResolve.bind(this);
this.onReject = this.onReject.bind(this);
this.setData = this.setData.bind(this);
this.setError = this.setError.bind(this);
const promise = props.promise;
const promiseFn = props.promiseFn || defaultProps.promiseFn;
const initialValue = props.initialValue || defaultProps.initialValue;
this.mounted = false;
this.counter = 0;
this.args = [];
this.abortController = {
abort: () => {}
};
this.state = _objectSpread$1({}, init({
initialValue,
promise,
promiseFn
}), {
cancel: this.cancel,
run: this.run,
reload: () => {
this.load();
this.run(...this.args);
},
setData: this.setData,
setError: this.setError
});
this.dispatch = (action, callback) => this.setState(state => reducer(state, action), callback);
}
componentDidMount() {
this.mounted = true;
if (this.props.promise || !this.state.initialValue) {
this.load();
}
}
componentDidUpdate(prevProps) {
const _this$props = this.props,
watch = _this$props.watch,
_this$props$watchFn = _this$props.watchFn,
watchFn = _this$props$watchFn === void 0 ? defaultProps.watchFn : _this$props$watchFn,
promise = _this$props.promise,
promiseFn = _this$props.promiseFn;
if (watch !== prevProps.watch) this.load();
if (watchFn && watchFn(_objectSpread$1({}, defaultProps, this.props), _objectSpread$1({}, defaultProps, prevProps))) this.load();
if (promise !== prevProps.promise) {
if (promise) this.load();else this.cancel();
}
if (promiseFn !== prevProps.promiseFn) {
if (promiseFn) this.load();else this.cancel();
}
}
componentWillUnmount() {
this.cancel();
this.mounted = false;
}
start() {
if ("AbortController" in window) {
this.abortController.abort();
this.abortController = new window.AbortController();
}
this.counter++;
this.mounted && this.dispatch({
type: actionTypes.start,
meta: {
counter: this.counter
}
});
}
load() {
const promise = this.props.promise;
if (promise) {
this.start();
return promise.then(this.onResolve(this.counter), this.onReject(this.counter));
}
const promiseFn = this.props.promiseFn || defaultProps.promiseFn;
if (promiseFn) {
this.start();
return promiseFn(this.props, this.abortController).then(this.onResolve(this.counter), this.onReject(this.counter));
}
}
run(...args) {
const deferFn = this.props.deferFn || defaultProps.deferFn;
if (deferFn) {
this.args = args;
this.start();
return deferFn(args, _objectSpread$1({}, defaultProps, this.props), this.abortController).then(this.onResolve(this.counter), this.onReject(this.counter));
}
}
cancel() {
this.counter++;
this.abortController.abort();
this.mounted && this.dispatch({
type: actionTypes.cancel,
meta: {
counter: this.counter
}
});
}
onResolve(counter) {
return data => {
if (this.counter === counter) {
const onResolve = this.props.onResolve || defaultProps.onResolve;
this.setData(data, () => onResolve && onResolve(data));
}
return data;
};
}
onReject(counter) {
return error => {
if (this.counter === counter) {
const onReject = this.props.onReject || defaultProps.onReject;
this.setError(error, () => onReject && onReject(error));
}
return error;
};
}
setData(data, callback) {
this.mounted && this.dispatch({
type: actionTypes.fulfill,
payload: data
}, callback);
return data;
}
setError(error, callback) {
this.mounted && this.dispatch({
type: actionTypes.reject,
payload: error,
error: true
}, callback);
return error;
}
render() {
const children = this.props.children;
if (isFunction(children)) {
return React__default.createElement(Provider, {
value: this.state
}, children(this.state));
}
if (children !== undefined && children !== null) {
return React__default.createElement(Provider, {
value: this.state
}, children);
}
return null;
}
}
if (PropTypes) {
Async.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
promise: PropTypes.instanceOf(Promise),
promiseFn: PropTypes.func,
deferFn: PropTypes.func,
watch: PropTypes.any,
watchFn: PropTypes.func,
initialValue: PropTypes.any,
onResolve: PropTypes.func,
onReject: PropTypes.func
};
}
/**
* Renders only when no promise has started or completed yet.
*
* @prop {Function|Node} children Function (passing state) or React node
* @prop {boolean} persist Show until we have data, even while pending (loading) or when an error occurred
*/
const Initial = ({
children,
persist
}) => React__default.createElement(Consumer, null, state => state.isInitial || persist && !state.data ? renderFn(children, state) : null);
if (PropTypes) {
Initial.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
persist: PropTypes.bool
};
}
/**
* Renders only while pending (promise is loading).
*
* @prop {Function|Node} children Function (passing state) or React node
* @prop {boolean} initial Show only on initial load (data/error is undefined)
*/
const Pending = ({
children,
initial
}) => React__default.createElement(Consumer, null, state => state.isPending && (!initial || !state.value) ? renderFn(children, state) : null);
if (PropTypes) {
Pending.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
initial: PropTypes.bool
};
}
/**
* Renders only when promise is resolved.
*
* @prop {Function|Node} children Function (passing data and state) or React node
* @prop {boolean} persist Show old data while pending (promise is loading)
*/
const Fulfilled = ({
children,
persist
}) => React__default.createElement(Consumer, null, state => state.isFulfilled || persist && state.data ? renderFn(children, state.data, state) : null);
if (PropTypes) {
Fulfilled.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
persist: PropTypes.bool
};
}
/**
* Renders only when promise is rejected.
*
* @prop {Function|Node} children Function (passing error and state) or React node
* @prop {boolean} persist Show old error while pending (promise is loading)
*/
const Rejected = ({
children,
persist
}) => React__default.createElement(Consumer, null, state => state.isRejected || persist && state.error ? renderFn(children, state.error, state) : null);
if (PropTypes) {
Rejected.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
persist: PropTypes.bool
};
}
/**
* Renders only when promise is fulfilled or rejected.
*
* @prop {Function|Node} children Function (passing state) or React node
* @prop {boolean} persist Continue rendering while loading new data
*/
const Settled = ({
children,
persist
}) => React__default.createElement(Consumer, null, state => state.isSettled || persist && state.value ? renderFn(children, state) : null);
if (PropTypes) {
Settled.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
persist: PropTypes.bool
};
}
Initial.displayName = `${displayName}.Initial`;
Pending.displayName = `${displayName}.Pending`;
Fulfilled.displayName = `${displayName}.Fulfilled`;
Rejected.displayName = `${displayName}.Rejected`;
Settled.displayName = `${displayName}.Settled`;
Async.displayName = displayName;
Async.Initial = Initial;
Async.Pending = Pending;
Async.Loading = Pending; // alias
Async.Fulfilled = Fulfilled;
Async.Resolved = Fulfilled; // alias
Async.Rejected = Rejected;
Async.Settled = Settled;
return Async;
};
var Async = createInstance();
function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest();
}
function _arrayWithHoles(arr) {
if (Array.isArray(arr)) return arr;
}
function _iterableToArrayLimit(arr, i) {
var _arr = [];
var _n = true;
var _d = false;
var _e = undefined;
try {
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"] != null) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance");
}
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$2(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$2(target, key, source[key]);
});
}
return target;
}
function _defineProperty$2(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;
}
const noop = () => {};
const useAsync = (arg1, arg2) => {
const counter = React.useRef(0);
const isMounted = React.useRef(true);
const lastArgs = React.useRef(undefined);
const prevOptions = React.useRef(undefined);
const abortController = React.useRef({
abort: noop
});
const options = typeof arg1 === "function" ? _objectSpread$2({}, arg2, {
promiseFn: arg1
}) : arg1;
const promise = options.promise,
promiseFn = options.promiseFn,
deferFn = options.deferFn,
initialValue = options.initialValue,
onResolve = options.onResolve,
onReject = options.onReject,
watch = options.watch,
watchFn = options.watchFn;
const _useReducer = React.useReducer(reducer, options, init),
_useReducer2 = _slicedToArray(_useReducer, 2),
state = _useReducer2[0],
dispatch = _useReducer2[1];
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
}
});
};
React.useEffect(() => {
if (watchFn && prevOptions.current && watchFn(options, prevOptions.current)) load();
});
React.useEffect(() => {
promise || promiseFn ? load() : cancel();
}, [promise, promiseFn, watch]);
React.useEffect(() => () => isMounted.current = false, []);
React.useEffect(() => () => abortController.current.abort(), []);
React.useEffect(() => (prevOptions.current = options) && undefined);
React.useDebugValue(state, ({
status
}) => `[${counter.current}] ${status}`);
return React.useMemo(() => _objectSpread$2({}, 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 = _ref.defer,
json = _ref.json,
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$2({}, options, {
[fn]: React.useCallback((_, props, ctrl) => doFetch(input, _objectSpread$2({
signal: ctrl ? ctrl.signal : props.signal
}, init)), [JSON.stringify(input), JSON.stringify(init)])
}));
React.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.");
};
var useAsync$1 = React.useEffect ? useAsync : unsupported;
const useFetch = React.useEffect ? useAsyncFetch : unsupported;
exports.createInstance = createInstance;
exports.default = Async;
exports.statusTypes = statusTypes;
exports.useAsync = useAsync$1;
exports.useFetch = useFetch;