UNPKG

react-accio

Version:

Declaratively fetch multiple APIs with a single React component.

670 lines (559 loc) 18.6 kB
import _regeneratorRuntime from 'babel-runtime/regenerator'; import { createElement, Component, forwardRef, createContext } from 'react'; import hoistStatics from 'hoist-non-react-statics'; function to(promise) { return promise.then(function (data) { return [null, data]; }).catch(function (err) { return [err]; }); } var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var asyncToGenerator = function (fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }; var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _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; }; var inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }; var objectWithoutProperties = function (obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }; var possibleConstructorReturn = function (self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }; var slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined$1; 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"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); // default resolver var resolver = (function () { var _ref = asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(url, fetchOptions) { var _ref2, _ref3, err, res, _ref4, _ref5, err2, jsonResponse; return _regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return to(fetch(url, fetchOptions)); case 2: _ref2 = _context.sent; _ref3 = slicedToArray(_ref2, 2); err = _ref3[0]; res = _ref3[1]; if (!err) { _context.next = 8; break; } throw new Error('Accio error: ' + err.message); case 8: if (!(res.ok === false)) { _context.next = 10; break; } throw new Error('Accio failed to fetch: ' + res.url + ' ' + res.status + ' (' + res.statusText + ')'); case 10: _context.next = 12; return to(res.json()); case 12: _ref4 = _context.sent; _ref5 = slicedToArray(_ref4, 2); err2 = _ref5[0]; jsonResponse = _ref5[1]; if (!err2) { _context.next = 18; break; } throw new Error('Error parsing response to json: ' + err2.message); case 18: return _context.abrupt('return', jsonResponse); case 19: case 'end': return _context.stop(); } } }, _callee, this); })); function resolver(_x, _x2) { return _ref.apply(this, arguments); } return resolver; })(); function validateFunctionAssignment(target, name) { return { get: function get$$1() { return target[name]; }, set: function set$$1(value) { if (typeof value !== 'function') { throw new TypeError('Expected ' + name + ' to be a function. But instead got ' + (typeof value === 'undefined' ? 'undefined' : _typeof(value)) + '. Check your ' + name + ' assignment to Accio defaults.'); } target[name] = value; } }; } // private, do not expose var _defaults = { resolver: resolver, method: 'GET' }; var defaults$1 = {}; Object.defineProperties(defaults$1, { resolver: validateFunctionAssignment(_defaults, 'resolver'), method: { get: function get() { return _defaults.method; }, set: function set(method) { if (!['GET', 'POST'].includes(method)) { throw new TypeError('Invalid method ' + method + '. Only GET & POST are supported. Check your method assignment to Accio defaults.'); } _defaults.method = method; } } }); function getCacheKey(url, fetchOptions) { var cacheKey = url; if (fetchOptions.body) { cacheKey = cacheKey + JSON.stringify(fetchOptions.body); } return cacheKey; } // $FlowFixMe var _React$createContext = createContext(null), Provider = _React$createContext.Provider, Consumer = _React$createContext.Consumer; function configureCache(type) { if (type === 'memory') { return new Map(); } if (type === 'localStorage') { throw new Error('Local storage cache support is incomplete. Please use memory in the meantime.'); } throw new Error('Unknown AccioCache storage type :/'); } var AccioCacheProvider = function (_React$Component) { inherits(AccioCacheProvider, _React$Component); function AccioCacheProvider() { var _ref; var _temp, _this, _ret; classCallCheck(this, AccioCacheProvider); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this = possibleConstructorReturn(this, (_ref = AccioCacheProvider.__proto__ || Object.getPrototypeOf(AccioCacheProvider)).call.apply(_ref, [this].concat(args))), _this), _this.cache = configureCache(_this.props.use), _temp), possibleConstructorReturn(_this, _ret); } createClass(AccioCacheProvider, [{ key: 'render', value: function render() { return createElement( Provider, { value: this.cache }, this.props.children ); } }]); return AccioCacheProvider; }(Component); AccioCacheProvider.defaultProps = { use: 'memory' }; var AccioCacheConsumer = Consumer; var AccioPropKeys = new Set(['children', 'url', 'context', 'defer', 'ignoreCache', 'onComplete', 'onError', 'onShowLoading', 'onStartFetching', 'timeout', '_cache']); function getFetchOptions(props) { var fetchOptions = {}; var propKeys = Object.keys(props); for (var i = 0; i < propKeys.length; i++) { var propKey = propKeys[i]; if (!AccioPropKeys.has(propKey)) { fetchOptions[propKey] = props[propKey]; } } return fetchOptions; } var PreloadStatus = { PRELOAD_ERROR: -1, IDLE: 0, PRELOADING: 1, PRELOADED: 2 }; var Accio = function (_React$Component) { inherits(Accio, _React$Component); function Accio() { var _ref; var _temp, _this, _ret; classCallCheck(this, Accio); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this = possibleConstructorReturn(this, (_ref = Accio.__proto__ || Object.getPrototypeOf(Accio)).call.apply(_ref, [this].concat(args))), _this), _this.state = { loading: false, response: null, error: null, trigger: _this.doWork.bind(_this) }, _this.preloadStatus = PreloadStatus.IDLE, _this.preloadError = null, _this.requestId = 0, _temp), possibleConstructorReturn(_this, _ret); } createClass(Accio, [{ key: 'preload', value: function () { var _ref2 = asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() { var _cache, _ref3, _ref4, err, res; return _regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _cache = this.props._cache; if (_cache) { _context.next = 4; break; } console.warn('Preloading without cache is not supported. ' + 'This can be fixed by wrapping your app with <AccioCacheProvider />.'); return _context.abrupt('return'); case 4: if (!(this.preloadStatus < PreloadStatus.PRELOADING)) { _context.next = 18; break; } this.preloadStatus = PreloadStatus.PRELOADING; _context.next = 8; return to(this.doFetch.call(this)); case 8: _ref3 = _context.sent; _ref4 = slicedToArray(_ref3, 2); err = _ref4[0]; res = _ref4[1]; if (!err) { _context.next = 16; break; } this.preloadStatus = PreloadStatus.PRELOAD_ERROR; this.preloadError = err; return _context.abrupt('return'); case 16: this.preloadStatus = PreloadStatus.PRELOADED; return _context.abrupt('return', res); case 18: case 'end': return _context.stop(); } } }, _callee, this); })); function preload() { return _ref2.apply(this, arguments); } return preload; }() }, { key: 'componentDidMount', value: function componentDidMount() { if (this.props.defer === true) { return; } this.doWork.call(this); } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { if (this.timer) { clearTimeout(this.timer); } } }, { key: 'componentDidUpdate', value: function componentDidUpdate(prevProps) { var fetchKey = this.props.fetchKey; if (fetchKey && fetchKey(this.props) !== fetchKey(prevProps)) { this.doWork.call(this); } } }, { key: 'doWork', value: function () { var _ref5 = asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2() { var newRequestId, _props, _cache, onStartFetching, timeout, url, cacheKey, preloadedResponse, _ref6, _ref7, err, response; return _regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: newRequestId = ++this.requestId; _props = this.props, _cache = _props._cache, onStartFetching = _props.onStartFetching, timeout = _props.timeout, url = _props.url; cacheKey = getCacheKey(url, getFetchOptions(this.props)); if (!(_cache && this.preloadStatus === PreloadStatus.PRELOADED)) { _context2.next = 7; break; } preloadedResponse = _cache.get(cacheKey); this.setResponse.call(this, preloadedResponse); return _context2.abrupt('return'); case 7: if (typeof timeout === 'number') { this.timer = setTimeout(this.setLoading.bind(this, true), timeout); } else { this.setLoading.call(this, true); } if (typeof onStartFetching === 'function') { onStartFetching(); } _context2.next = 11; return to(this.doFetch.call(this)); case 11: _ref6 = _context2.sent; _ref7 = slicedToArray(_ref6, 2); err = _ref7[0]; response = _ref7[1]; if (!(newRequestId !== this.requestId)) { _context2.next = 17; break; } return _context2.abrupt('return'); case 17: if (err) { this.setError.call(this, err); } if (this.timer) { clearTimeout(this.timer); } this.setLoading.call(this, false); this.setResponse.call(this, response); case 21: case 'end': return _context2.stop(); } } }, _callee2, this); })); function doWork() { return _ref5.apply(this, arguments); } return doWork; }() }, { key: 'doFetch', value: function doFetch() { var _props2 = this.props, _cache = _props2._cache, context = _props2.context, ignoreCache = _props2.ignoreCache, url = _props2.url, onStartFetching = _props2.onStartFetching; var resolver = Accio.defaults.resolver; var fetchOptions = getFetchOptions(this.props); // try resolve from cache, // otherwise resolve from network var resolveNetwork = function resolveNetwork() { return resolver(url, fetchOptions, context); }; if (_cache && ignoreCache === false) { var cacheKey = getCacheKey(url, fetchOptions); // check for existing cache entry if (_cache.has(cacheKey)) { // cache hit --> return cached entry return Promise.resolve(_cache.get(cacheKey)); } else { // cache miss --> resolve network var promise = resolveNetwork(); // store promise in cache _cache.set(cacheKey, promise); return promise.then(function (response) { // when resolved, store the real // response to the cache _cache.set(cacheKey, response); return response; }).catch(function (err) { _cache.delete(cacheKey); throw err; }); } } return resolveNetwork(); } }, { key: 'setLoading', value: function setLoading(loading) { if (typeof this.props.onShowLoading === 'function') { this.props.onShowLoading(); } this.setState({ loading: loading }); } }, { key: 'setResponse', value: function setResponse(response) { if (typeof this.props.onComplete === 'function') { this.props.onComplete(response); } this.setState({ response: response }); } }, { key: 'setError', value: function setError(error) { if (typeof this.props.onError === 'function') { this.props.onError(error); } this.setState({ error: error }); } }, { key: 'render', value: function render() { return this.props.children(this.state); } }]); return Accio; }(Component); Accio.defaults = defaults$1; Accio.defaultProps = { defer: false, ignoreCache: false, context: {}, method: Accio.defaults.method }; var CachedAccio = function (_React$Component) { inherits(CachedAccio, _React$Component); function CachedAccio() { classCallCheck(this, CachedAccio); return possibleConstructorReturn(this, (CachedAccio.__proto__ || Object.getPrototypeOf(CachedAccio)).apply(this, arguments)); } createClass(CachedAccio, [{ key: 'renderChild', value: function renderChild(_cache) { var _props = this.props, children = _props.children, forwardedRef = _props.forwardedRef, props = objectWithoutProperties(_props, ['children', 'forwardedRef']); return createElement( Accio, _extends({}, props, { ref: forwardedRef, _cache: _cache }), children ); } }, { key: 'render', value: function render() { return createElement( AccioCacheConsumer, null, this.renderChild.bind(this) ); } }]); return CachedAccio; }(Component); // $FlowFixMe https://github.com/facebook/flow/issues/6103 var CachedAccioWithRef = forwardRef(function (props, ref) { return createElement(CachedAccio, _extends({}, props, { forwardedRef: ref })); }); hoistStatics(CachedAccioWithRef, Accio); export { CachedAccioWithRef as Accio, AccioCacheProvider };