react-request
Version:
Declarative HTTP requests with React.
503 lines (421 loc) • 17.8 kB
JavaScript
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 _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; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(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; }
function _inherits(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; }
import React from 'react';
import PropTypes from 'prop-types';
import { getRequestKey, fetchDedupe, isRequestInFlight } from 'fetch-dedupe';
// This object is our cache
// The keys of the object are requestKeys
// The value of each key is a Response instance
var responseCache = {};
// The docs state that this is not safe to use in an
// application. That's just because I am not writing tests,
// nor designing the API, around folks clearing the cache.
// This was only added to help out with testing your app.
// Use your judgment if you decide to use this in your
// app directly.
export function clearResponseCache() {
responseCache = {};
}
export var Fetch = function (_React$Component) {
_inherits(Fetch, _React$Component);
_createClass(Fetch, [{
key: 'render',
value: function render() {
// Anything pulled from `this.props` here is not eligible to be
// specified when calling `doFetch`.
var _props = this.props,
children = _props.children,
requestName = _props.requestName;
var _state = this.state,
fetching = _state.fetching,
response = _state.response,
data = _state.data,
error = _state.error,
requestKey = _state.requestKey,
url = _state.url;
if (!children) {
return null;
} else {
return children({
requestName: requestName,
url: url,
fetching: fetching,
failed: Boolean(error || response && !response.ok),
response: response,
data: data,
requestKey: requestKey,
error: error,
doFetch: this.fetchRenderProp
}) || null;
}
}
}]);
function Fetch(props, context) {
_classCallCheck(this, Fetch);
var _this = _possibleConstructorReturn(this, (Fetch.__proto__ || Object.getPrototypeOf(Fetch)).call(this, props, context));
_this.isReadRequest = function (method) {
var uppercaseMethod = method.toUpperCase();
return uppercaseMethod === 'GET' || uppercaseMethod === 'HEAD' || uppercaseMethod === 'OPTIONS';
};
_this.isLazy = function () {
var _this$props = _this.props,
lazy = _this$props.lazy,
method = _this$props.method;
return typeof lazy === 'undefined' ? !_this.isReadRequest(method) : lazy;
};
_this.shouldCacheResponse = function () {
var _this$props2 = _this.props,
cacheResponse = _this$props2.cacheResponse,
method = _this$props2.method;
return typeof cacheResponse === 'undefined' ? _this.isReadRequest(method) : cacheResponse;
};
_this.getFetchPolicy = function () {
var _this$props3 = _this.props,
fetchPolicy = _this$props3.fetchPolicy,
method = _this$props3.method;
if (typeof fetchPolicy === 'undefined') {
return _this.isReadRequest(method) ? 'cache-first' : 'network-only';
} else {
return fetchPolicy;
}
};
_this.cancelExistingRequest = function (reason) {
if (_this.state.fetching && _this._currentRequestKey !== null) {
var abortError = new Error(reason);
// This is an effort to mimic the error that is created when a
// fetch is actually aborted using the AbortController API.
abortError.name = 'AbortError';
_this.onResponseReceived(_extends({}, _this.responseReceivedInfo, {
error: abortError,
hittingNetwork: true
}));
}
};
_this.fetchRenderProp = function (options) {
return new Promise(function (resolve) {
// We wrap this in a setTimeout so as to avoid calls to `setState`
// in render, which React does not allow.
//
// tl;dr, the following code should never cause a React warning or error:
//
// `<Fetch children={({ doFetch }) => doFetch()} />
setTimeout(function () {
_this.fetchData(options, true, resolve);
});
});
};
_this.getRequestKey = function (options) {
// A request key in the options gets top priority
if (options && options.requestKey) {
return options.requestKey;
}
// Otherwise, if we have no request key, but we do have options, then we
// recompute the request key based on these options.
// Note that if the URL, body, or method have not changed, then the request
// key should match the previous request key if it was computed.
// If you passed in a custom request key as a prop, then you will also
// need to pass in a custom key when you call `doFetch()`!
else if (options) {
var _Object$assign = Object.assign({}, _this.props, options),
url = _Object$assign.url,
method = _Object$assign.method,
body = _Object$assign.body;
return getRequestKey({
url: url,
body: body,
method: method.toUpperCase()
});
}
// Next in line is the the request key from props.
else if (_this.props.requestKey) {
return _this.props.requestKey;
}
// Lastly, we compute the request key from the props.
else {
var _this$props4 = _this.props,
_url = _this$props4.url,
_method = _this$props4.method,
_body = _this$props4.body;
return getRequestKey({
url: _url,
body: _body,
method: _method.toUpperCase()
});
}
};
_this.fetchData = function (options, ignoreCache, resolve) {
// These are the things that we do not allow a user to configure in
// `options` when calling `doFetch()`. Perhaps we should, however.
var _this$props5 = _this.props,
requestName = _this$props5.requestName,
dedupe = _this$props5.dedupe,
beforeFetch = _this$props5.beforeFetch;
_this.cancelExistingRequest('New fetch initiated');
var requestKey = _this.getRequestKey(options);
var requestOptions = Object.assign({}, _this.props, options);
_this._currentRequestKey = requestKey;
var url = requestOptions.url,
body = requestOptions.body,
credentials = requestOptions.credentials,
headers = requestOptions.headers,
method = requestOptions.method,
responseType = requestOptions.responseType,
mode = requestOptions.mode,
cache = requestOptions.cache,
redirect = requestOptions.redirect,
referrer = requestOptions.referrer,
referrerPolicy = requestOptions.referrerPolicy,
integrity = requestOptions.integrity,
keepalive = requestOptions.keepalive,
signal = requestOptions.signal;
var uppercaseMethod = method.toUpperCase();
var shouldCacheResponse = _this.shouldCacheResponse();
var init = {
body: body,
credentials: credentials,
headers: headers,
method: uppercaseMethod,
mode: mode,
cache: cache,
redirect: redirect,
referrer: referrer,
referrerPolicy: referrerPolicy,
integrity: integrity,
keepalive: keepalive,
signal: signal
};
var responseReceivedInfo = {
url: url,
init: init,
requestKey: requestKey,
responseType: responseType
};
// This is necessary because `options` may have overridden the props.
// If the request config changes, we need to be able to accurately
// cancel the in-flight request.
_this.responseReceivedInfo = responseReceivedInfo;
var fetchPolicy = _this.getFetchPolicy();
var cachedResponse = void 0;
if (fetchPolicy !== 'network-only' && !ignoreCache) {
cachedResponse = responseCache[requestKey];
if (cachedResponse) {
_this.onResponseReceived(_extends({}, responseReceivedInfo, {
response: cachedResponse,
hittingNetwork: false,
stillFetching: fetchPolicy === 'cache-and-network'
}));
if (fetchPolicy === 'cache-first' || fetchPolicy === 'cache-only') {
return Promise.resolve(cachedResponse);
}
} else if (fetchPolicy === 'cache-only') {
var cacheError = new Error('Response for "' + requestName + '" not found in cache.');
_this.onResponseReceived(_extends({}, responseReceivedInfo, {
error: cacheError,
hittingNetwork: false
}));
return Promise.resolve(cacheError);
}
}
_this.setState({
requestKey: requestKey,
url: url,
error: null,
failed: false,
fetching: true
});
var hittingNetwork = !isRequestInFlight(requestKey) || !dedupe;
if (hittingNetwork) {
beforeFetch({
url: url,
init: init,
requestKey: requestKey
});
}
return fetchDedupe(url, init, { requestKey: requestKey, responseType: responseType, dedupe: dedupe }).then(function (res) {
if (shouldCacheResponse) {
responseCache[requestKey] = res;
}
if (_this._currentRequestKey === requestKey) {
_this.onResponseReceived(_extends({}, responseReceivedInfo, {
response: res,
hittingNetwork: hittingNetwork,
resolve: resolve
}));
}
return res;
}, function (error) {
if (_this._currentRequestKey === requestKey) {
_this.onResponseReceived(_extends({}, responseReceivedInfo, {
error: error,
cachedResponse: cachedResponse,
hittingNetwork: hittingNetwork,
resolve: resolve
}));
}
return error;
});
};
_this.onResponseReceived = function (info) {
var _info$error = info.error,
error = _info$error === undefined ? null : _info$error,
_info$response = info.response,
response = _info$response === undefined ? null : _info$response,
hittingNetwork = info.hittingNetwork,
url = info.url,
init = info.init,
requestKey = info.requestKey,
cachedResponse = info.cachedResponse,
_info$stillFetching = info.stillFetching,
stillFetching = _info$stillFetching === undefined ? false : _info$stillFetching,
resolve = info.resolve;
_this.responseReceivedInfo = null;
if (!stillFetching) {
_this._currentRequestKey = null;
}
var data = void 0;
// If our response succeeded, then we use that data.
if (response && response.data) {
data = response.data;
} else if (cachedResponse && cachedResponse.data) {
// This happens when the request failed, but we have cache-and-network
// specified. Although we pass along the failed response, we continue to
// pass in the cached data.
data = cachedResponse.data;
}
data = data ? _this.props.transformData(data) : null;
// If we already have some data in state on error, then we continue to
// pass that data down. This prevents the data from being wiped when a
// request fails, which is generally not what people want.
// For more, see: GitHub Issue #154
if (error && _this.state.data) {
data = _this.state.data;
}
var afterFetchInfo = {
url: url,
init: init,
requestKey: requestKey,
error: error,
failed: Boolean(error || response && !response.ok),
response: response,
data: data,
didUnmount: Boolean(_this.willUnmount)
};
if (typeof resolve === 'function') {
resolve(afterFetchInfo);
}
if (hittingNetwork) {
_this.props.afterFetch(afterFetchInfo);
}
if (_this.willUnmount) {
return;
}
_this.setState({
url: url,
data: data,
error: error,
response: response,
fetching: stillFetching,
requestKey: requestKey
}, function () {
return _this.props.onResponse(error, response);
});
};
_this.state = {
requestKey: props.requestKey || getRequestKey(_extends({}, props, {
method: props.method.toUpperCase()
})),
requestName: props.requestName,
fetching: false,
response: null,
data: null,
error: null,
url: props.url
};
return _this;
}
// We default to being lazy for "write" requests,
// such as POST, PATCH, DELETE, and so on.
_createClass(Fetch, [{
key: 'componentDidMount',
value: function componentDidMount() {
if (!this.isLazy()) {
this.fetchData();
}
}
// Because we use `componentDidUpdate` to determine if we should fetch
// again, there will be at least one render when you receive your new
// fetch options, such as a new URL, but the fetch has not begun yet.
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate(prevProps) {
var currentRequestKey = this.props.requestKey || getRequestKey(_extends({}, this.props, {
method: this.props.method.toUpperCase()
}));
var prevRequestKey = prevProps.requestKey || getRequestKey(_extends({}, prevProps, {
method: prevProps.method.toUpperCase()
}));
if (currentRequestKey !== prevRequestKey && !this.isLazy()) {
this.fetchData({
requestKey: currentRequestKey
});
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
this.willUnmount = true;
this.cancelExistingRequest('Component unmounted');
}
// When a request is already in flight, and a new one is
// configured, then we need to "cancel" the previous one.
// When a subsequent request is made, it is important that the correct
// request key is used. This method computes the right key based on the
// options and props.
}]);
return Fetch;
}(React.Component);
var globalObj = typeof self !== 'undefined' ? self : this;
var AbortSignalCtr = globalObj !== undefined ? globalObj.AbortSignal : function () {};
Fetch.propTypes = {
children: PropTypes.func,
requestName: PropTypes.string,
fetchPolicy: PropTypes.oneOf(['cache-first', 'cache-and-network', 'network-only', 'cache-only']),
onResponse: PropTypes.func,
beforeFetch: PropTypes.func,
afterFetch: PropTypes.func,
responseType: PropTypes.oneOfType([PropTypes.func, PropTypes.oneOf(['json', 'text', 'blob', 'arrayBuffer', 'formData'])]),
transformData: PropTypes.func,
lazy: PropTypes.bool,
dedupe: PropTypes.bool,
requestKey: PropTypes.string,
url: PropTypes.string.isRequired,
body: PropTypes.any,
credentials: PropTypes.oneOf(['omit', 'same-origin', 'include']),
headers: PropTypes.object,
method: PropTypes.oneOf(['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']),
mode: PropTypes.oneOf(['same-origin', 'cors', 'no-cors', 'navigate', 'websocket']),
cache: PropTypes.oneOf(['default', 'no-store', 'reload', 'no-cache', 'force-cache', 'only-if-cached']),
redirect: PropTypes.oneOf(['manual', 'follow', 'error']),
referrer: PropTypes.string,
referrerPolicy: PropTypes.oneOf(['no-referrer', 'no-referrer-when-downgrade', 'origin', 'origin-when-cross-origin', 'unsafe-url', '']),
integrity: PropTypes.string,
keepalive: PropTypes.bool,
signal: PropTypes.instanceOf(AbortSignalCtr)
};
Fetch.defaultProps = {
requestName: 'anonymousRequest',
onResponse: function onResponse() {},
beforeFetch: function beforeFetch() {},
afterFetch: function afterFetch() {},
transformData: function transformData(data) {
return data;
},
dedupe: true,
method: 'get',
referrerPolicy: '',
integrity: '',
referrer: 'about:client'
};