react-accio
Version:
Declaratively fetch multiple APIs with a single React component.
670 lines (559 loc) • 18.6 kB
JavaScript
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 };