UNPKG

react-async

Version:

React component for declarative promise resolution and data fetching

337 lines (283 loc) 9.94 kB
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 React from "react"; import { actionTypes, init, reducer } from "./reducer.js"; 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. */ export const createInstance = (defaultProps = {}, displayName = "Async") => { const { Consumer, Provider } = React.createContext(); class Async extends React.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({}, 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 { watch, watchFn = defaultProps.watchFn, promise, promiseFn } = this.props; if (watch !== prevProps.watch) this.load(); if (watchFn && watchFn(_objectSpread({}, defaultProps, this.props), _objectSpread({}, 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({}, 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; if (isFunction(children)) { return React.createElement(Provider, { value: this.state }, children(this.state)); } if (children !== undefined && children !== null) { return React.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.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.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.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.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.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; }; export default createInstance();