UNPKG

react-request

Version:
503 lines (421 loc) 17.8 kB
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' };