UNPKG

react-request

Version:
458 lines (385 loc) 14.2 kB
'use strict'; exports.__esModule = true; exports.Fetch = undefined; 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; }; exports.clearResponseCache = clearResponseCache; var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); var _fetchDedupe = require('fetch-dedupe'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // This object is our cache // The keys of the object are requestKeys // The value of each key is a Response instance let 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. function clearResponseCache() { responseCache = {}; } class Fetch extends _react2.default.Component { render() { // Anything pulled from `this.props` here is not eligible to be // specified when calling `doFetch`. const { children, requestName } = this.props; const { fetching, response, data, error, requestKey, url } = this.state; if (!children) { return null; } else { return children({ requestName, url, fetching, failed: Boolean(error || response && !response.ok), response, data, requestKey, error, doFetch: this.fetchRenderProp }) || null; } } constructor(props, context) { super(props, context); this.isReadRequest = method => { const uppercaseMethod = method.toUpperCase(); return uppercaseMethod === 'GET' || uppercaseMethod === 'HEAD' || uppercaseMethod === 'OPTIONS'; }; this.isLazy = () => { const { lazy, method } = this.props; return typeof lazy === 'undefined' ? !this.isReadRequest(method) : lazy; }; this.shouldCacheResponse = () => { const { cacheResponse, method } = this.props; return typeof cacheResponse === 'undefined' ? this.isReadRequest(method) : cacheResponse; }; this.getFetchPolicy = () => { const { fetchPolicy, method } = this.props; if (typeof fetchPolicy === 'undefined') { return this.isReadRequest(method) ? 'cache-first' : 'network-only'; } else { return fetchPolicy; } }; this.cancelExistingRequest = reason => { if (this.state.fetching && this._currentRequestKey !== null) { const 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 = options => { return new Promise(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(() => { this.fetchData(options, true, resolve); }); }); }; this.getRequestKey = 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) { const { url, method, body } = Object.assign({}, this.props, options); return (0, _fetchDedupe.getRequestKey)({ url, 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 { const { url, method, body } = this.props; return (0, _fetchDedupe.getRequestKey)({ url, body, method: method.toUpperCase() }); } }; this.fetchData = (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. const { requestName, dedupe, beforeFetch } = this.props; this.cancelExistingRequest('New fetch initiated'); const requestKey = this.getRequestKey(options); const requestOptions = Object.assign({}, this.props, options); this._currentRequestKey = requestKey; const { url, body, credentials, headers, method, responseType, mode, cache, redirect, referrer, referrerPolicy, integrity, keepalive, signal } = requestOptions; const uppercaseMethod = method.toUpperCase(); const shouldCacheResponse = this.shouldCacheResponse(); const init = { body, credentials, headers, method: uppercaseMethod, mode, cache, redirect, referrer, referrerPolicy, integrity, keepalive, signal }; const responseReceivedInfo = { url, init, requestKey, 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; const fetchPolicy = this.getFetchPolicy(); let cachedResponse; 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') { const 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, url, error: null, failed: false, fetching: true }); const hittingNetwork = !(0, _fetchDedupe.isRequestInFlight)(requestKey) || !dedupe; if (hittingNetwork) { beforeFetch({ url, init, requestKey }); } return (0, _fetchDedupe.fetchDedupe)(url, init, { requestKey, responseType, dedupe }).then(res => { if (shouldCacheResponse) { responseCache[requestKey] = res; } if (this._currentRequestKey === requestKey) { this.onResponseReceived(_extends({}, responseReceivedInfo, { response: res, hittingNetwork, resolve })); } return res; }, error => { if (this._currentRequestKey === requestKey) { this.onResponseReceived(_extends({}, responseReceivedInfo, { error, cachedResponse, hittingNetwork, resolve })); } return error; }); }; this.onResponseReceived = info => { const { error = null, response = null, hittingNetwork, url, init, requestKey, cachedResponse, stillFetching = false, resolve } = info; this.responseReceivedInfo = null; if (!stillFetching) { this._currentRequestKey = null; } let data; // 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; } const afterFetchInfo = { url, init, requestKey, error, failed: Boolean(error || response && !response.ok), response, data, didUnmount: Boolean(this.willUnmount) }; if (typeof resolve === 'function') { resolve(afterFetchInfo); } if (hittingNetwork) { this.props.afterFetch(afterFetchInfo); } if (this.willUnmount) { return; } this.setState({ url, data, error, response, fetching: stillFetching, requestKey }, () => this.props.onResponse(error, response)); }; this.state = { requestKey: props.requestKey || (0, _fetchDedupe.getRequestKey)(_extends({}, props, { method: props.method.toUpperCase() })), requestName: props.requestName, fetching: false, response: null, data: null, error: null, url: props.url }; } // We default to being lazy for "write" requests, // such as POST, PATCH, DELETE, and so on. 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. componentDidUpdate(prevProps) { const currentRequestKey = this.props.requestKey || (0, _fetchDedupe.getRequestKey)(_extends({}, this.props, { method: this.props.method.toUpperCase() })); const prevRequestKey = prevProps.requestKey || (0, _fetchDedupe.getRequestKey)(_extends({}, prevProps, { method: prevProps.method.toUpperCase() })); if (currentRequestKey !== prevRequestKey && !this.isLazy()) { this.fetchData({ requestKey: currentRequestKey }); } } 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. } exports.Fetch = Fetch; const globalObj = typeof self !== 'undefined' ? self : undefined; const AbortSignalCtr = globalObj !== undefined ? globalObj.AbortSignal : function () {}; Fetch.propTypes = { children: _propTypes2.default.func, requestName: _propTypes2.default.string, fetchPolicy: _propTypes2.default.oneOf(['cache-first', 'cache-and-network', 'network-only', 'cache-only']), onResponse: _propTypes2.default.func, beforeFetch: _propTypes2.default.func, afterFetch: _propTypes2.default.func, responseType: _propTypes2.default.oneOfType([_propTypes2.default.func, _propTypes2.default.oneOf(['json', 'text', 'blob', 'arrayBuffer', 'formData'])]), transformData: _propTypes2.default.func, lazy: _propTypes2.default.bool, dedupe: _propTypes2.default.bool, requestKey: _propTypes2.default.string, url: _propTypes2.default.string.isRequired, body: _propTypes2.default.any, credentials: _propTypes2.default.oneOf(['omit', 'same-origin', 'include']), headers: _propTypes2.default.object, method: _propTypes2.default.oneOf(['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']), mode: _propTypes2.default.oneOf(['same-origin', 'cors', 'no-cors', 'navigate', 'websocket']), cache: _propTypes2.default.oneOf(['default', 'no-store', 'reload', 'no-cache', 'force-cache', 'only-if-cached']), redirect: _propTypes2.default.oneOf(['manual', 'follow', 'error']), referrer: _propTypes2.default.string, referrerPolicy: _propTypes2.default.oneOf(['no-referrer', 'no-referrer-when-downgrade', 'origin', 'origin-when-cross-origin', 'unsafe-url', '']), integrity: _propTypes2.default.string, keepalive: _propTypes2.default.bool, signal: _propTypes2.default.instanceOf(AbortSignalCtr) }; Fetch.defaultProps = { requestName: 'anonymousRequest', onResponse: () => {}, beforeFetch: () => {}, afterFetch: () => {}, transformData: data => data, dedupe: true, method: 'get', referrerPolicy: '', integrity: '', referrer: 'about:client' };