UNPKG

react-async

Version:

React component for declarative promise resolution and data fetching

771 lines (652 loc) 21.5 kB
'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;