├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── ROADMAP.md ├── docs ├── FAQ.md ├── examples.md └── guides │ ├── INDEX.md │ ├── aborting.md │ ├── best-practices.md │ ├── differences-with-apollo.md │ ├── differences-with-fetch.md │ ├── integration-with-other-technologies.md │ ├── request-deduplication.md │ ├── request-keys.md │ ├── response-caching.md │ └── using-the-lazy-prop.md ├── examples ├── fetch-components │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.js │ │ ├── fetch-components │ │ └── posts.js │ │ └── index.js ├── lazy-read │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.js │ │ └── index.js ├── multiple-requests │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.js │ │ └── index.js ├── request-deduplication │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.js │ │ └── index.js ├── response-caching │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.js │ │ └── index.js ├── simple-read │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.js │ │ └── index.js └── updating-a-resource │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ └── src │ ├── App.js │ └── index.js ├── jest.config.js ├── package.json ├── rollup.config.js ├── src ├── fetch.js └── index.js └── test ├── .eslintrc ├── do-fetch.test.js ├── index.test.js ├── responses.js ├── same-component.test.js └── setup.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "build": { 4 | "presets": [ 5 | ["env", { 6 | "modules": false 7 | }], "stage-3", "react" 8 | ], 9 | "plugins": ["external-helpers", "transform-class-properties"] 10 | }, 11 | "buildProd": { 12 | "presets": [ 13 | ["env", { 14 | "modules": false 15 | }], "stage-3", "react" 16 | ], 17 | "plugins": [ 18 | "external-helpers", 19 | "transform-class-properties", [ 20 | "transform-react-remove-prop-types", 21 | { 22 | "mode": "remove", 23 | "removeImport": true 24 | } 25 | ] 26 | ] 27 | }, 28 | "es": { 29 | "presets": [ 30 | ["env", { 31 | "modules": false 32 | }], "stage-3", "react" 33 | ], 34 | "plugins": ["transform-class-properties"] 35 | }, 36 | "commonjs": { 37 | "plugins": [ 38 | ["transform-es2015-modules-commonjs", { 39 | "loose": true 40 | }], 41 | "transform-class-properties" 42 | ], 43 | "presets": ["stage-3", "react"] 44 | }, 45 | "test": { 46 | "presets": ["env", "stage-3", "react"], 47 | "plugins": ["transform-class-properties"] 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint:recommended", 4 | "parserOptions": { 5 | "ecmaVersion": 8, 6 | "sourceType": "module", 7 | "ecmaFeatures": { 8 | "experimentalObjectRestSpread": true 9 | } 10 | }, 11 | "rules": { 12 | "no-unused-vars": "error", 13 | "no-use-before-define": "error", 14 | "react/jsx-uses-react": "error", 15 | "react/jsx-uses-vars": "error" 16 | }, 17 | "env": { 18 | "browser": true, 19 | "node": true 20 | }, 21 | "globals": { 22 | "Promise": true 23 | }, 24 | "plugins": ["react"] 25 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | .vscode 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | package-lock.json 26 | package-lock.json.* 27 | dist 28 | es 29 | lib -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxBracketSameLine": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | sudo: false 5 | notifications: 6 | email: false 7 | after_success: 8 | # Upload to coveralls, but don't _fail_ if coveralls is down. 9 | - cat coverage/lcov.info | node_modules/.bin/coveralls || echo "Coveralls upload failed" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### v3.2.0 (2021/1/11) 4 | 5 | - React 17 is now included in peer dependencies. 6 | 7 | ### v3.1.2 (2018/8/2) 8 | 9 | **Bug Fixes** 10 | 11 | - Fixes a bug that can occur in React Native environments 12 | 13 | ### v3.1.1 (2018/7/10) 14 | 15 | **Bug Fixes** 16 | 17 | - A race condition has been fixed. 18 | 19 | ### v3.1.0 (2018/7/10) 20 | 21 | **New Features** 22 | 23 | - `doFetch` now returns a Promise that _always_ resolves. The value that it resolves to is 24 | the same object that is passed to `afterFetch`. Note that `afterFetch` is only called when a 25 | network request has actually been performed, whereas `doFetch` resolves even when the cache is hit. 26 | 27 | ### v3.0.1 (2018/4/24) 28 | 29 | **Bug Fixes** 30 | 31 | - Fixes a bug where `isLazy` would sometimes be computed using previous 32 | props rather than the current props. 33 | 34 | ### v3.0.0 (2018/4/24) 35 | 36 | > Although the changes in this release are technically breaking, they are unlikely to 37 | > affect most users' code. 38 | 39 | **Breaking Changes** 40 | 41 | - When a request fails, the `data` from a request will no longer be set to `null`. This 42 | allows you to control whether or not your UI continues to display the existing data. 43 | 44 | - The `responseType` prop is now more forgiving. If the body cannot be parsed with 45 | the `responseType` that you set, then `data` will be set to `null`. 46 | 47 | ### v2.0.4 (2018/4/20) 48 | 49 | **Bug Fixes** 50 | 51 | - Fixes a bug where there could be a cache mismatch when re-rendering the same component 52 | that has a fetch policy configured. 53 | 54 | ### v2.0.3 (2018/3/2) 55 | 56 | **Bug Fixes** 57 | 58 | - Fixes a bug where the `lazy` prop was not always respected. Anytime that a new request key was generated, 59 | a request would be made. 60 | 61 | ### v2.0.2 (2018/2/21) 62 | 63 | **Bug Fixes** 64 | 65 | - Fixes a bug where an Uncaught ReferenceError could be thrown 66 | 67 | ### v2.0.1 (2018/2/17) 68 | 69 | **Bug Fixes** 70 | 71 | - This fixes a problem where the default `fetchPolicy` would be `"cache-first"` for "write" requests. 72 | 73 | ### v2.0.0 (2018/2/17) 74 | 75 | **Breaking** 76 | 77 | - `transformResponse` has been renamed to be `transformData` 78 | - `fetchPolicy` is now determined by the method that you pass in. This change was made to support using 79 | POST methods for read requests, and is unlikely to break your code. 80 | - A new prop, `cacheResponse`, is used to determine if a response is added to the cache or 81 | not. This is to support using POST methods for read requests, and is unlikely to break your code. 82 | 83 | **New Features** 84 | 85 | - A new `failed` property is passed to you in the render prop callback. This allows you to 86 | quickly determine if a request failed for any reason (be it network errors or "error" status 87 | codes). 88 | 89 | ### v1.1.0 (2018/2/7) 90 | 91 | **New Features** 92 | 93 | - `responseType` can now be specified as a function. It receives the `response` 94 | as the first argument. 95 | - Adds a `requestKey` prop. 96 | - When the request is "faux-aborted," the error will have a `name` equal to `AbortError`. 97 | This matches the name of the native error, allowing you to write future-proof code that 98 | handles aborted requests. 99 | 100 | ### v1.0.0 (2018/2/4) 101 | 102 | **Breaking** 103 | 104 | - The `responseType` will now be set to `"text"` anytime a response returns 105 | with a 204 status code. 106 | - The `responseType` is no longer used when creating the request key. 107 | 108 | ### v0.3.0 (2018/2/4) 109 | 110 | **Changes** 111 | 112 | - `fetch-dedupe` has been abstracted into a separate library. This 113 | does not change the public API of this library. 114 | 115 | ### v0.2.0 (2018/2/1) 116 | 117 | **New Features** 118 | 119 | - The render prop will now be passed the `requestKey`. 120 | 121 | ### v0.1.0 (2018/2/1) 122 | 123 | React's new Context API has been finalized, and it uses functional `children` rather than a prop 124 | named `render`. Accordingly, this library has been updated to use `children` as the default. 125 | 126 | **Breaking** 127 | 128 | - `` now uses `children` as the render prop, rather than `render`. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 James, please 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Request 2 | 3 | [![Travis build status](http://img.shields.io/travis/jamesplease/react-request.svg?style=flat)](https://travis-ci.org/jamesplease/react-request) 4 | [![npm version](https://img.shields.io/npm/v/react-request.svg)](https://www.npmjs.com/package/react-request) 5 | [![Test Coverage](https://coveralls.io/repos/github/jamesplease/react-request/badge.svg?branch=master)](https://coveralls.io/github/jamesplease/react-request?branch=master) 6 | [![gzip size](http://img.badgesize.io/https://unpkg.com/react-request/dist/react-request.min.js?compression=gzip)](https://unpkg.com/react-request/dist/react-request.min.js) 7 | 8 | Declarative HTTP requests for React. 9 | 10 | ### Motivation 11 | 12 | Making a single HTTP request is not difficult to do in JavaScript. However, 13 | complex web applications often make many requests as the 14 | user navigates through the app. 15 | 16 | Features such as request deduplication and response caching can often save the 17 | developer of apps like these from headache and bugs. Although it is possible to 18 | implement these features imperatively, it requires that you write a bit of 19 | code, and that code can be tedious to test. 20 | 21 | A declarative API makes things a lot simpler for you, which is where React Request 22 | comes in. React Request is a backend-agnostic, declarative solution for HTTP 23 | requests in React, and its deduping and caching features are a delight to use. 24 | 25 | ### Features 26 | 27 | ✓ Uses the native [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API 28 | ✓ Smart [deduping of requests](./docs/guides/request-deduplication.md) 29 | ✓ Customizable [response caching](./docs/guides/response-caching.md) 30 | ✓ Integrates with external stores (such as [Redux](https://github.com/reactjs/redux)) 31 | ✓ Reasonable footprint (~2kb gzipped) 32 | 33 | ### Installation 34 | 35 | Install using [npm](https://www.npmjs.com): 36 | 37 | ``` 38 | npm install react-request 39 | ``` 40 | 41 | or [yarn](https://yarnpkg.com/): 42 | 43 | ``` 44 | yarn add react-request 45 | ``` 46 | 47 | ### Documentation 48 | 49 | - [**Getting Started**](#getting-started) 50 | - [**API**](#api) 51 | - [\](#fetch-) 52 | - [props](#props) 53 | - [Arguments passed to the render prop](#propschildren) 54 | - [Using `doFetch`](#using-dofetch) 55 | - [fetchDedupe()](#fetchdedupe-input--init--dedupeoptions-) 56 | - [getRequestKey()](#getrequestkey-url-method-body-responsetype-) 57 | - [isRequestInFlight()](#isrequestinflight-requestkey-) 58 | - [clearRequestCache()](#clearrequestcache) 59 | - [clearResponseCache()](#clearresponsecache) 60 | - [**Guides ⇗**](./docs/guides/INDEX.md) 61 | - [Response Caching ⇗](./docs/guides/response-caching.md) 62 | - [Request Deduplication ⇗](./docs/guides/request-deduplication.md) 63 | - [Request Keys ⇗](./docs/guides/request-keys.md) 64 | - [Best Practices ⇗](./docs/guides/best-practices.md) 65 | - [Using the `lazy` Prop ⇗](./docs/guides/using-the-lazy-prop.md) 66 | - [Aborting ⇗](./docs/guides/aborting.md) 67 | - [Differences with `fetch()` ⇗](./docs/guides/differences-with-fetch.md) 68 | - [Differences with React Apollo ⇗](./docs/guides/differences-with-apollo.md) 69 | - [Integration with Other Technologies ⇗](./docs/guides/integration-with-other-technologies.md) 70 | - [**Examples ⇗**](./docs/examples.md) 71 | - [**FAQ ⇗**](./docs/FAQ.md) 72 | - [**Roadmap ⇗**](./ROADMAP.md) 73 | - [**Acknowledgements**](#acknowledgements) 74 | 75 | ### Getting Started 76 | 77 | Here's a quick look at what using React Request is like: 78 | 79 | ```jsx 80 | import React, { Component } from 'react'; 81 | import { Fetch } from 'react-request'; 82 | 83 | class App extends Component { 84 | render() { 85 | return ( 86 | 87 | {({ fetching, failed, data }) => { 88 | if (fetching) { 89 | return
Loading data...
; 90 | } 91 | 92 | if (failed) { 93 | return
The request did not succeed.
; 94 | } 95 | 96 | if (data) { 97 | return ( 98 |
99 |
Post ID: {data.id}
100 |
Post Title: {data.title}
101 |
102 | ); 103 | } 104 | 105 | return null; 106 | }} 107 |
108 | ); 109 | } 110 | } 111 | ``` 112 | 113 | Need to make multiple requests? You can use any tool that you would like that 114 | allows you to "compose" [render prop components](https://reactjs.org/docs/render-props.html) together. This example 115 | uses [React Composer](https://github.com/jamesplease/react-composer): 116 | 117 | ```jsx 118 | import React, { Component } from 'react'; 119 | import Composer from 'react-composer'; 120 | 121 | class App extends Component { 122 | render() { 123 | return ( 124 | , 127 | , 131 | ]}> 132 | {([readPost, deletePost]) => { 133 | return ( 134 |
135 | {readPost.fetching && 'Loading post 1'} 136 | {!readPost.fetching && 'Post 1 is not being fetched'} 137 | 140 |
141 | ); 142 | }} 143 |
144 | ); 145 | } 146 | } 147 | ``` 148 | 149 | These examples just scratch the surface of what React Request can do for you. 150 | Check out the API reference below, or 151 | [read the guides](https://github.com/jamesplease/react-request/blob/master/docs/guides/INDEX.md), 152 | to learn more. 153 | 154 | ### API 155 | 156 | #### `` 157 | 158 | A component for making a single HTTP request. This is the export from this library that you will use 159 | most frequently. 160 | 161 | ```jsx 162 | import { Fetch } from 'react-request'; 163 | ``` 164 | 165 | ##### Props 166 | 167 | The `` components accepts every value of `init` and `input` 168 | from the 169 | [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) 170 | API as a prop, in addition to a few other things. 171 | 172 | The complete list of props is: 173 | 174 | | Prop | Default value | Description | 175 | | ------------------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 176 | | url | | From [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). The URL to send the request to | 177 | | method | `'GET'` | From [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). The HTTP method to use | 178 | | body | | From [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). The request body to send along with the request | 179 | | credentials | | From [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). The request credentials you want to use for the request: `omit`, `same-origin`, or `include.` | 180 | | headers | | From [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). Headers to send along with the request | 181 | | [children](#propschildren) | | A function that is called with a single argument containing information about the request. Learn more. | 182 | | [lazy](#propslazy) | _Varies_ | Whether or not the request is made when the component mounts. | 183 | | [beforeFetch](#propsbeforefetch) | | A function called before a network request is made. | 184 | | [afterFetch](#propsafterfetch) | | A function that is only called after a network request is made. | 185 | | [onResponse](#propsonresponse) | | A function called anytime a response is received, whether from the network or cache. | 186 | | [transformData](#propstransformdata) | | A function that is called with the body of the response, allowing you to transform it. | 187 | | [responseType](#propsresponsetype) | `'json'` | Whether or not the request is made when the component mounts. | 188 | | [requestName](#propsrequestname) | | A name to give this request, which can be useful for debugging. | 189 | | [fetchPolicy](#propsfetchpolicy) | | The cache strategy to use. | 190 | | [cacheResponse](#propscacheresponse) | _Varies_ | Whether or not to cache the response for this request. | 191 | | [dedupe](#propsdedupe) | `true` | Whether or not to dedupe this request. | 192 | | [requestKey](#propsrequestkey) | _Generated_ | A key that is used for deduplication and response caching. | 193 | | mode | | From [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). The mode you want to use for the request | 194 | | cache | | From [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). The browser's cache mode you want to use for the request | 195 | | redirect | | From [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). The redirect mode to use | 196 | | referrer | `'about:client'` | From [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). The referrer to use for the request | 197 | | referrerPolicy | `''` | From [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). Specifies the value of the referer HTTP header. | 198 | | integrity | `''` | From [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). Contains the [subresource integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) value of the request | 199 | | keepalive | | From [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). Can be used to allow the request to outlive the page | 200 | | signal | | From [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). An AbortSignal object instance | 201 | 202 | To learn more about the valid options for the props that come from `fetch`, refer to the 203 | [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) 204 | documentation. 205 | 206 | The following example demonstrates some of the most commonly-used props that come from `fetch()`: 207 | 208 | ```jsx 209 | 217 | {({ doFetch }) => { 218 | ; 219 | }} 220 | 221 | ``` 222 | 223 | In addition to the `fetch()` props, there are a number of other useful props. 224 | 225 | ##### `props.children` 226 | 227 | `children` is the [render prop](https://reactjs.org/docs/render-props.html) of this component. 228 | It is called with one argument, `result`, an object with the following keys: 229 | 230 | | Key | Type | Description | 231 | | ----------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 232 | | fetching | Boolean | A Boolean representing whether or not a request is currently in flight for this component | 233 | | failed | Boolean | A Boolean representing whether or not the request failed for any reason. This includes network errors and status codes that are greater than or equal to`400`. | 234 | | error | Object | An error object representing a network error occurred. Note that HTTP "error" status codes do not cause errors; only failed or aborted network requests do. For more, see the ["Using Fetch" MDN guide](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful). | 235 | | response | Object | An instance of [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). The [`body`](https://developer.mozilla.org/en-US/docs/Web/API/Body) will already be read, and made available to you as `response.data`. | 236 | | data | Object | The data returned in `response`. This will be different from `response.data` if a `transformData` prop was passed to ``. | 237 | | doFetch | Function | A function that allows you to manually make the HTTP request. [Read more.](#using-dofetch) | 238 | | url | String | The URL that was passed as a prop to `` | 239 | | requestName | String | The name of the request (see `requestName` below) | 240 | | requestKey | String | The [request key](./docs/guides/request-keys.md) of the request | 241 | 242 | ###### Using doFetch 243 | 244 | There are three common use cases for the `doFetch` prop: 245 | 246 | - You can use it to "refresh" the data by making a follow-up request for read requests 247 | - You can use it to retry the request if there is any sort of error 248 | - You must manually call this method to actually make the request anytime that the `lazy` prop 249 | is passed as `true`. 250 | 251 | `doFetch` accepts one argument: `options`. Any of the `fetch()` options, such as `url`, `method`, and 252 | `body` are valid `options`. You may also specify a new `requestKey` if you are manually generating your 253 | own keys. This method allows you to customize the request from within the component based on the 254 | component's state. 255 | 256 | `doFetch` returns a Promise that **always** resolves. It resolves to the same argument that the 257 | [`afterFetch`](#afterFetch) prop receives. 258 | 259 | In the following example, we demonstrate how you can modify the request by passing options to `doFetch`. 260 | 261 | ```jsx 262 | 263 | {({ doFetch }) => ( 264 | // You can pass options to `doFetch` to customize the request. All of the props from `fetch()`, such as `url`, 265 | // `body`, and so on, are supported. 266 | 269 | )} 270 | 271 | ``` 272 | 273 | ##### `props.lazy` 274 | 275 | Whether or not the request will be called when the component mounts. The default value 276 | is based on the request method that you use. 277 | 278 | | Method | Default value | 279 | | ------------------------ | ------------- | 280 | | GET, HEAD, OPTIONS | `false` | 281 | | POST, PUT, PATCH, DELETE | `true` | 282 | 283 | ```jsx 284 | 285 | {({ doFetch }) => { 286 | ; 287 | }} 288 | 289 | ``` 290 | 291 | ##### `props.beforeFetch` 292 | 293 | A function that is called just before a network request is initiated. It is called 294 | with one argument, an object with the following keys: 295 | 296 | - `url`: The URL of the request 297 | - `init`: The second argument passed to `global.fetch()`, which specifies things 298 | such as the body, method, and so on 299 | - `requestKey`: Either the computed request key, or the value of the 300 | `requestKey` prop 301 | 302 | This feature is useful for analytics, or syncing response data with a data store such 303 | as [Redux](https://github.com/reactjs/redux/). 304 | 305 | > Note: This function is not called when the component reads from the cache. 306 | 307 | ##### `props.afterFetch` 308 | 309 | A function that is called anytime that a network response is received. It is called 310 | with one argument, an object with the following keys: 311 | 312 | - `url`: The URL of the request 313 | - `init`: The second argument passed to `global.fetch()`, which specifies things 314 | such as the body, method, and so on 315 | - `requestKey`: Either the computed request key, or the value of the 316 | `requestKey` prop 317 | - `response`: The response that was received from the HTTP request 318 | - `data`: The transformed data from the response. This will be different from 319 | `response.data` if a `transformData` function was passed as a prop to ``. 320 | - `error`: An error returned from the HTTP request 321 | - `didUnmount`: A Boolean representing whether or not the component has unmounted 322 | 323 | This can be used for analytics or syncing response data with a data store such 324 | as [Redux](https://github.com/reactjs/redux/). 325 | 326 | > Note: This function is not called when the component reads from the cache. 327 | 328 | ##### `props.onResponse` 329 | 330 | A function that is called every time a response is received, whether that 331 | response is from the cache or from a network request. Receives two arguments: 332 | `error` and `response`. 333 | 334 | ```jsx 335 | { 338 | if (error) { 339 | console.log('Ruh roh', error); 340 | } else { 341 | console.log('Got a response!', response); 342 | } 343 | }}> 344 | {() => { 345 |
Hello
; 346 | }} 347 |
348 | ``` 349 | 350 | ##### `props.transformData` 351 | 352 | A function that is called with the data returned from the response. You can use this 353 | hook to transform the data before it is passed into `children`. 354 | 355 | ```jsx 356 | data.post> 359 | {({ fetching, error, response, data }) => { 360 |
361 | {fetching && ('Loading...')} 362 | {error && ('There was an error.')} 363 | {!fetching && !error && response.status === 200 && ( 364 |
365 |

{data.title}

366 |
{data.content}
367 |
368 | )} 369 |
370 | }} 371 |
372 | ``` 373 | 374 | > Note: `transformData` does not modify the value of `response.data`. The transformed data is 375 | > made available to you in the [render prop argument](#children) under the `data` key. 376 | 377 | ##### `props.responseType` 378 | 379 | The content type of the response body. Defaults to `"json"` unless the response has a 204 status code, 380 | in which case it will be `"text"` instead. Valid values are any of the methods on 381 | [Body](https://developer.mozilla.org/en-US/docs/Web/API/Body). 382 | 383 | Alternatively, you may specify a function that returns a string. The function will be called with one 384 | argument: `response`. This allows you to dynamically specify the response type based on information 385 | about the response, such as its status code. 386 | 387 | ```jsx 388 | // If you have an endpoint that just returns raw text, you could, for instance, convert it into 389 | // an object using `responseType` and `transformData`. 390 | { 394 | return { 395 | countryName, 396 | }; 397 | }}> 398 | {({ data }) => { 399 | if (data) { 400 | return
{data.countryName}
; 401 | } 402 | 403 | return null; 404 | }} 405 |
406 | ``` 407 | 408 | If the response body cannot be parsed as the `responseType` that you specify, then `data` will 409 | be set to `null`. 410 | 411 | ##### `props.requestName` 412 | 413 | A name to give this request, which can help with debugging purposes. The request name is 414 | analogous to a function name in JavaScript. Although we could use anonymous functions 415 | everywhere, we tend to give them names to help humans read and debug the code. 416 | 417 | ```jsx 418 | 419 | ``` 420 | 421 | > Note: This feature is analogous to the [operation name](http://graphql.org/learn/queries/#operation-name) in GraphQL. 422 | 423 | ##### `props.fetchPolicy` 424 | 425 | This determines how the request interacts with the cache. Valid options are: 426 | 427 | - `"cache-first"` 428 | - `"cache-and-network"` 429 | - `"network-only"` 430 | - `"cache-only"` 431 | 432 | For documentation on what each of these values do, refer to the [response caching guide](./docs/guides/response-caching.md). 433 | 434 | The default value of this prop is based on the value of the `method` prop that you pass to ``. 435 | 436 | | Method | Default value | 437 | | ------------------------ | ---------------- | 438 | | GET, HEAD, OPTIONS | `"cache-first"` | 439 | | POST, PUT, PATCH, DELETE | `"network-only"` | 440 | 441 | > This prop behaves identically to the Apollo prop 442 | > [with the same name](https://www.apollographql.com/docs/react/basics/queries.html#graphql-config-options-fetchPolicy). 443 | 444 | ##### `props.cacheResponse` 445 | 446 | Whether or not the response will be cached. The default value is based on the value of the `method` prop that you pass 447 | to ``. 448 | 449 | | Method | Default value | 450 | | ------------------------ | ------------- | 451 | | GET, HEAD, OPTIONS | `true` | 452 | | POST, PUT, PATCH, DELETE | `false` | 453 | 454 | For documentation on this prop, refer to the [response caching guide](./docs/guides/response-caching.md). 455 | 456 | ##### `props.dedupe` 457 | 458 | A Boolean value representing whether or not the request should be 459 | [deduplicated](./docs/guides/request-deduplication.md). 460 | Defaults to `true`. 461 | 462 | ##### `props.requestKey` 463 | 464 | A string that is used to control the request deduplication and response caching features. By default, 465 | a key is generated for you. Specifying a custom key is an advanced feature that you may not need. 466 | 467 | For more, see the [request key](https://github.com/jamesplease/react-request/blob/master/docs/guides/request-keys.md) 468 | guide. 469 | 470 | --- 471 | 472 | The rest of the API documentation describes the other named exports from the `react-request` package. Typically, 473 | you won't need to use these, but they are available should you need them. 474 | 475 | #### `fetchDedupe( input [, init] [, dedupeOptions] )` 476 | 477 | This is the `fetchDedupe` export from the [Fetch Dedupe](https://github.com/jamesplease/fetch-dedupe) 478 | library. Fetch Dedupe powers the request deduplication in React Request. 479 | 480 | Whenever you need to make a standalone HTTP request outside of the 481 | `` component, then you can use this with confidence that you won't send a 482 | duplicate request. 483 | 484 | For more, refer to [the documentation of fetch-dedupe](https://github.com/jamesplease/fetch-dedupe). 485 | 486 | #### `getRequestKey({ url, method, body, responseType })` 487 | 488 | Generates a request key. All of the values are optional. You typically never need to use this, as request 489 | keys are generated automatically for you when you use React Request or Fetch Dedupe. 490 | 491 | This method comes from [`fetch-dedupe`](https://github.com/jamesplease/fetch-dedupe). 492 | 493 | #### `isRequestInFlight( requestKey )` 494 | 495 | Return a Boolean representing if a request for `requestKey` is in flight or not. 496 | 497 | This method comes from [`fetch-dedupe`](https://github.com/jamesplease/fetch-dedupe). 498 | 499 | #### `clearRequestCache()` 500 | 501 | Wipes the cache of deduped requests. Mostly useful for testing. 502 | 503 | This method comes from [`fetch-dedupe`](https://github.com/jamesplease/fetch-dedupe). 504 | 505 | > Note: this method is not safe to use in application code. 506 | 507 | #### `clearResponseCache()` 508 | 509 | Wipes the cache of cached responses. Mostly useful for testing. 510 | 511 | > Note: this method is not safe to use in application code. 512 | 513 | ### Acknowledgements 514 | 515 | This library was inspired by [Apollo](https://www.apollographql.com). The 516 | library [Holen](https://github.com/tkh44/holen) was referenced during the 517 | creation of this library. 518 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | This roadmap outlines future changes to React Request. 4 | 5 | ### Next release 6 | 7 | * [ ] `doFetch` will return a Promise 8 | * [ ] A serial requests story will be more defined. Right now, it isn't super clear how to do it. 9 | 10 | ### Sometime later 11 | 12 | * [ ] The caching implementation will be moved to `fetch-dedupe`. This is a non-breaking change. 13 | * [ ] Caching and deduplication will support an array, allowing for a more sophisticated system that 14 | supports multi-operational HTTP requests (GraphQL, for instance) 15 | * [ ] Implement "true" aborting rather than the "fake" aborting currently in the lib 16 | * [ ] Add an Abort API 17 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | Here are some answers to common questions. Don't see yours here? 4 | [Open an issue](https://github.com/jamesplease/react-request/issues/new) and 5 | we would be happy to help. 6 | 7 | #### Why was this created? 8 | 9 | While I was studying Apollo, a framework for GraphQL, I learned that its React library exports a 10 | higher-order component that enables a developer to declaratively specify HTTP requests. It surprised me 11 | that this was the primary method that Apollo developers were interfacing with GraphQL over HTTP. 12 | 13 | It didn't take long for me to understand that the Apollo HoC was remarkably useful. And although some of the features 14 | that it provides only make sense in the context of GraphQL, I noticed that other features, such as request 15 | deduplication and response caching, would be incredibly useful to any developer who is writing React code. 16 | 17 | The only problem is that you can't use Apollo's HoC unless you use GraphQL. This is why I wrote React Request: 18 | to abstract this functionality out into a general-use component. 19 | 20 | #### Why would anyone want to use JSX for making HTTP requests? 21 | 22 | I was skeptical at first, too, but it turns out that many of the complex things people do with HTTP requests (namely, 23 | request deduplication and response caching) map nicely to the component lifecycle. You can remove a considerable amount 24 | of difficult-to-test imperative code from your application by declaratively specifying how you want your requests to 25 | behave through JSX. 26 | 27 | #### If a request is made when the component mounts, how does that work for POST, PATCH, or DELETE requests? 28 | 29 | The default behavior is that requests are only made on mount for `GET` requests when the component mounts, and 30 | not for those other HTTP methods. 31 | 32 | One of the things that is passed into the render prop function is a method called `fetch`. Calling this method will perform 33 | the request. This allows you to hook up, say, a PATCH request to a button. 34 | 35 | Whether or not the request makes a request on mount can be customized with the `lazy` prop. 36 | 37 | #### What if I just want a regular fetch component without deduplication or caching features? 38 | 39 | Take a look at [Holen](https://github.com/tkh44/holen). 40 | 41 | #### What about normalization of data? Apollo does this. 42 | 43 | That requires a definition of what a "resource" (or "entity") is, which is a little bit too opinionated for this 44 | library. 45 | 46 | With that said, this library is an excellent tool to use to build React bindings for a more opinionated framework 47 | that does define a resource or entity. For instance, the [Redux Resource](https://redux-resource.js.org) React bindings 48 | will be built using React Request. 49 | 50 | #### Why isn't this a higher-order component like Apollo's `graphql`? 51 | 52 | The render prop pattern is more powerful than Higher Order Components. Read 53 | [this post](https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce) for more. 54 | 55 | In an upcoming version of React Apollo, they, too, will export render prop components. 56 | 57 | #### Why is the prop named `children` rather than `render`? 58 | 59 | Although there are a handful of arguments for the render prop to be `render` rather than `children`, this library is 60 | using the same pattern that React's [new Context API](https://github.com/reactjs/rfcs/pull/2) uses. 61 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | A number of examples are distributed with the library's 4 | [source code](https://github.com/jamesplease/react-request). 5 | 6 | ### Simple Read 7 | 8 | To run this example: 9 | 10 | ``` 11 | git clone https://github.com/jamesplease/react-request.git 12 | 13 | cd react-request/examples/simple-read 14 | npm install 15 | npm start 16 | 17 | open http://localhost:3000/ 18 | ``` 19 | 20 | This example shows a simple request that fetches a single resource 21 | when the component mounts. 22 | 23 | ### Lazy Read 24 | 25 | To run this example: 26 | 27 | ``` 28 | git clone https://github.com/jamesplease/react-request.git 29 | 30 | cd react-request/examples/lazy-read 31 | npm install 32 | npm start 33 | 34 | open http://localhost:3000/ 35 | ``` 36 | 37 | In this example, the request is defined as lazy, so it isn't made 38 | when the component first mounts. Instead, the user of the application 39 | must click a button to make the request. 40 | 41 | ### Updating a Resource 42 | 43 | To run this example: 44 | 45 | ``` 46 | git clone https://github.com/jamesplease/react-request.git 47 | 48 | cd react-request/examples/updating-a-resource 49 | npm install 50 | npm start 51 | 52 | open http://localhost:3000/ 53 | ``` 54 | 55 | Requests with the headers `POST`, `PUT`, `PATCH`, or `DELETE` are lazy 56 | by default. This example demonstrates how these sorts of requests are 57 | typically used in your application. 58 | 59 | ### Multiple Requests 60 | 61 | To run this example: 62 | 63 | ``` 64 | git clone https://github.com/jamesplease/react-request.git 65 | 66 | cd react-request/examples/multiple-requests 67 | npm install 68 | npm start 69 | 70 | open http://localhost:3000/ 71 | ``` 72 | 73 | Sometimes, you will need access to multiple requests at the same time. This 74 | example demonstrates using the [React Composer](https://github.com/jamesplease/react-composer) 75 | library to do that. 76 | 77 | ### Request Deduplication 78 | 79 | To run this example: 80 | 81 | ``` 82 | git clone https://github.com/jamesplease/react-request.git 83 | 84 | cd react-request/examples/request-deduplication 85 | npm install 86 | npm start 87 | 88 | open http://localhost:3000/ 89 | ``` 90 | 91 | This example demonstrates how many requests of the same kind are deduplicated by default. 92 | 93 | ### Response Caching 94 | 95 | To run this example: 96 | 97 | ``` 98 | git clone https://github.com/jamesplease/react-request.git 99 | 100 | cd react-request/examples/response-caching 101 | npm install 102 | npm start 103 | 104 | open http://localhost:3000/ 105 | ``` 106 | 107 | Response caching comes built into React Request. This example shows the default 108 | `fetchPolicy` that returns data from the cache when it is available, rather than 109 | making a second request. 110 | 111 | ### Fetch Components 112 | 113 | To run this example: 114 | 115 | ``` 116 | git clone https://github.com/jamesplease/react-request.git 117 | 118 | cd react-request/examples/fetch-components 119 | npm install 120 | npm start 121 | 122 | open http://localhost:3000/ 123 | ``` 124 | 125 | Often times, configuring HTTP requests requires, well, a lot of configuration, such as headers, 126 | the URL, and the cache policy. 127 | 128 | Rather than repeating this throughout your app, you can clean things up by creating "fetch components," 129 | an organizational strategy where the `` components themselves are placed into standalone files. 130 | -------------------------------------------------------------------------------- /docs/guides/INDEX.md: -------------------------------------------------------------------------------- 1 | # Guides 2 | 3 | These guides provide tips and additional information that can help you use React Request. 4 | 5 | * [Response Caching](./response-caching.md) 6 | * [Request Deduplication](./request-deduplication.md) 7 | * [Request Keys](./request-keys.md) 8 | * [Best Practices](./best-practices.md) 9 | * [Using the `lazy` Prop](./using-the-lazy-prop.md) 10 | * [Aborting](./aborting.md) 11 | * [Differences with `fetch()`](./differences-with-fetch.md) 12 | * [Differences with React Apollo](./differences-with-react-apollo.md) 13 | * [Integration with Other Technologies](./integration-with-other-technologies.md) 14 | -------------------------------------------------------------------------------- /docs/guides/aborting.md: -------------------------------------------------------------------------------- 1 | # Aborting Requests 2 | 3 | Browsers will soon support an API to allow developers to 4 | [abort fetches](https://developers.google.com/web/updates/2017/09/abortable-fetch). Aborting 5 | requests within React Request currently is not currently supported, but it is on the 6 | project roadmap. 7 | 8 | For more, refer to [this GitHub issue](https://github.com/jamesplease/react-request/issues/26). 9 | 10 | ### Pseudo-aborts 11 | 12 | The `` component will "pseudo-abort" requests. With a faux-abort, the actual HTTP request 13 | will not be aborted, but there are situations when the response will be ignored: 14 | 15 | 1. When the component unmounts 16 | 2. When a new request is initiated while an existing request is already in flight 17 | 18 | In these situations, `onResponse` will be called with an error that has a `name` equal 19 | to `AbortError`. 20 | 21 | ### Warning: don't use the `signal` prop 22 | 23 | You may be tempted to use the `signal` prop of `` to abort requests, but this 24 | may not work as well as you would like it to. The `` component will make a new 25 | request anytime a new [request key](./request-keys.md) is generated from the props that 26 | you pass in. 27 | 28 | This means that you would need to create a new `AbortController` any time that there is 29 | a new request key. Although this may work for limited use cases, it does not scale well, 30 | so we do not recommend it as a general practice. 31 | -------------------------------------------------------------------------------- /docs/guides/best-practices.md: -------------------------------------------------------------------------------- 1 | # Best Practices 2 | 3 | Here are some tips that may help you when using React Request. 4 | 5 | ### Handling errors 6 | 7 | The `failed` Boolean is passed to the `children` callback, which 8 | represents whether _any_ error occurred with the request. This includes 9 | network requests or status codes greater than or equal to 400. 10 | 11 | This Boolean is convenient for a coarse understanding that the network 12 | failed, but using this Boolean alone is typically not enough to provide 13 | a great user experience. A user may want to know why the request 14 | failed. Was the resource not found? Did the user submit bad information? 15 | Was there a network error? Was the user logged out? 16 | 17 | We encourage you to dig into the `error` and `response` objects 18 | to provide your users with a more detailed explanation of what went wrong, 19 | rather than displaying a generic "There was an error" message. 20 | 21 | Here is an example that shows the different kinds of ways that a response 22 | can error. 23 | 24 | ```js 25 | 26 | {({ failed, error, response }) => { 27 | if (failed) { 28 | console.log('There was _some_ kind of error. What happened?'); 29 | } 30 | 31 | if (error) { 32 | console.log('There was a network error'); 33 | 34 | if (!navigation.onLine) { 35 | console.log('The user lost internet connection.'); 36 | } else { 37 | // You can look at the Error object to learn even details. 38 | console.log('The request was aborted, or it timed out'); 39 | } 40 | } 41 | 42 | const status = response && response.status; 43 | 44 | if (status === 404) { 45 | console.log('The resource was not found.'); 46 | } else if (status === 401) { 47 | console.log('You user have been logged out.'); 48 | } else if (status === 400) { 49 | console.log('Invalid data was submitted'); 50 | } else if (status === 500) { 51 | console.log('Something went wrong on the server.'); 52 | } 53 | }} 54 | 55 | ``` 56 | 57 | ### Making "fetch components" 58 | 59 | HTTP requests require a lot of configuration, which can make your 60 | application code messy. One way to clean this up is to make 61 | "fetch components" that simplify the API to make a request. 62 | 63 | For instance, if your application manages books, you may have these 64 | fetch components: 65 | 66 | ```jsx 67 | // books.js 68 | import React from 'react'; 69 | import { Fetch } from 'react-request'; 70 | import headers from './utils/default-request-headers'; 71 | import httpAnalytics from './utils/http-analytics'; 72 | 73 | export function ReadBook({ bookId, children }) { 74 | return ( 75 | 82 | ); 83 | } 84 | 85 | export function DeleteBook({ bookId, children }) { 86 | return ( 87 | 95 | ); 96 | } 97 | ``` 98 | 99 | This makes it so you only need to specify things like credentials, 100 | headers, and analytics in a single place. 101 | 102 | You can use these components in your application like so: 103 | 104 | ```jsx 105 | import React, { Component } from 'react'; 106 | import { readBook } from './request-components/books'; 107 | 108 | export default class App extends Component { 109 | render() { 110 | const { bookId } = this.props; 111 | 112 | return ( 113 |
114 |

Welcome to My App

115 | 116 | {result => { 117 | // Use the result here 118 | }} 119 | 120 |
121 | ); 122 | } 123 | } 124 | ``` 125 | 126 | If you've used [Redux](https://redux.js.org) for HTTP requests in the past, then you can think of the 127 | "fetch components" as fulfilling a similar role as action creators. They cut down on the boilerplate. 128 | 129 | ### Directory Structure 130 | 131 | We recommend organizing your fetch components by their resource type. For instance, if your app manages 132 | books, authors, and publishers, you might have: 133 | 134 | ``` 135 | /src 136 | /fetch-components 137 | books.js 138 | authors.js 139 | publishers.js 140 | ``` 141 | 142 | Within each of those resource files, you might have multiple "fetch components" for reading and updating 143 | the resources in various ways. 144 | -------------------------------------------------------------------------------- /docs/guides/differences-with-apollo.md: -------------------------------------------------------------------------------- 1 | # Differences with Apollo 2 | 3 | React Apollo is a great library, and it served as the primary inspiration for React Request. 4 | There are a few places where this library is different from React Apollo. 5 | 6 | ### Missing Features 7 | 8 | React Request is intended to be relatively lightweight, so it does not implement these 9 | features or props from React Apollo: 10 | 11 | * Polling 12 | * Optimistic responses 13 | * `errorPolicy` prop 14 | * `notifyOnNetworkStatusChange` prop 15 | 16 | The reason for these omissions is that we believe that you can implement this features in your 17 | application, or in a wrapping component, without too much extra work. If you disagree, open an issue – 18 | we may very well be wrong! 19 | 20 | ### One component 21 | 22 | React Apollo provides two components, one for Queries and the other for Mutations. React Request provides 23 | just the one. When working with HTTP without the GraphQL, it doesn't make as much sense to split out reads 24 | and writes from one another. 25 | 26 | ### Requesting on mount 27 | 28 | With Apollo, whether or not the request is made when the component mounts depends on if it is a mutation or 29 | a query. React Request looks at the HTTP method instead to determine if the request is _likely_ a read request or a write request. 30 | 31 | ### No Data Normalization 32 | 33 | React Apollo normalizes your data, and makes it available through the store. This requires a 34 | definition of a "resource" or "entity," which is beyond the scope of this library. 35 | 36 | If you want data normalization, you need two things: 37 | 38 | 1. a location to place the normalized data in memory (typically called a store) 39 | 2. a library or framework that defines what a resource or entity is 40 | 41 | For instance, [Redux](https://redux.js.org) can fulfill the role of item 1 while 42 | [Redux Resource](https://redux-resource.js.org) can fulfill the role of item 2. 43 | 44 | Once you have those two things, you can use React Request to provide declarative 45 | requests (in fact, the official React bindings for Redux Resource will be built 46 | using React Request). 47 | -------------------------------------------------------------------------------- /docs/guides/differences-with-fetch.md: -------------------------------------------------------------------------------- 1 | # Differences with `fetch()` 2 | 3 | Whenever possible, we try to follow standard usage of the 4 | [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) 5 | API as closely as possible. There are a few differences between using `fetch()` and this 6 | library, though, which are explained here. 7 | 8 | ### `init` is not an Object 9 | 10 | The second argument to `fetch()` is an optional object called `init`. With 11 | ``, the object has been spread out as props. This change was made 12 | for aesthetic reasons: I find it ugly to pass objects as props. 13 | 14 | ```js 15 | fetch('/posts/2', { 16 | method: 'PATCH', 17 | credentials: 'include', 18 | body: JSON.stringify({ title: 'New post' }) 19 | }); 20 | ``` 21 | 22 | ```jsx 23 | 29 | ``` 30 | 31 | ### The response body is read for you 32 | 33 | When using `fetch`, you must manually read the body. This 34 | library reads it automatically for you. This is because the body 35 | is a ReadableStream, and can only be read a single time. It was 36 | a requirement that React Request read it for you to support 37 | [deduplication of requests](./request-deduplication.md). 38 | 39 | ```js 40 | fetch('/posts/2', { 41 | method: 'PATCH', 42 | credentials: 'include', 43 | body: JSON.stringify({ title: 'New post' }) 44 | }) 45 | .then(res => res.body.json()) 46 | .then(data => { 47 | console.log('Got my JSON', data); 48 | }); 49 | ``` 50 | 51 | ```jsx 52 | 57 | {({ data }) => { 58 | console.log('Got my JSON', data); 59 | return null; 60 | }} 61 | 62 | ``` 63 | 64 | ### Only string request bodies are supported 65 | 66 | For now, you may only pass strings as the request body. This is 67 | due to the fact that we need to build a request key for 68 | [request deduplication](./request-deduplication.md) and 69 | response caching. 70 | 71 | In the future, we plan to add support for additional request body types. 72 | 73 | ### Aborting requests 74 | 75 | When aborting `fetch()`, you will typically do the following: 76 | 77 | 1. Create an AbortController object 78 | 2. Pass the AbortSignal object into `fetch` 79 | 80 | This system does not work with ``, because it would be tedious 81 | for you to create a new AbortController anytime the component was going 82 | to make a new request. 83 | 84 | It is on our roadmap to provide a great story around aborting requests. For 85 | more, see the guide on [aborting requests](./aborting.md). 86 | -------------------------------------------------------------------------------- /docs/guides/integration-with-other-technologies.md: -------------------------------------------------------------------------------- 1 | # Integration with Other Technologies 2 | 3 | React Request was designed to integrate well with other tools in the ecosystem. 4 | 5 | ### GraphQL 6 | 7 | At the moment, a system like Apollo or Relay is better suited for GraphQL. 8 | 9 | The reason for this is that React Request only caches and dedupes at the _request level_, 10 | whereas a library like Apollo caches and dedupes at the sub-request level. 11 | 12 | This is important because GraphQL allows you to embed multiple operations into a single 13 | HTTP request. 14 | 15 | > Note: multiple operation request support is on the roadmap for this project. 16 | 17 | ### Redux 18 | 19 | Data returned by your server can be synchronized with your Redux store. The 20 | `beforeFetch` and `afterFetch` methods make this straightforward to do. 21 | 22 | The following example demonstrates how you might go about doing this: 23 | 24 | ```jsx 25 | { 28 | store.dispatch({ 29 | type: 'FETCH_PENDING' 30 | // ...add whatever you want here 31 | }); 32 | }} 33 | afterFetch={data => { 34 | if (data.error || (data.response && !data.response.ok)) { 35 | store.dispatch({ 36 | type: 'FETCH_FAILED' 37 | // ...add other things to this action 38 | }); 39 | } else { 40 | store.dispatch({ 41 | type: 'FETCH_SUCCEEDED' 42 | // ...add other things to this action 43 | }); 44 | } 45 | }} 46 | /> 47 | ``` 48 | 49 | ### Redux Resource 50 | 51 | React Request was built with [Redux Resource](https://redux-resource.js.org) in mind. 52 | An official library that provides React bindings for Redux Resource is in the works, and it 53 | will use React Request. 54 | 55 | ### React Native 56 | 57 | React Request may work in React Native. I am not sure, because I haven't used React Native 58 | before. 59 | 60 | Before you try it out, be sure to include the polyfill for 61 | [a global object](https://github.com/johanneslumpe/react-native-browser-polyfill/blob/25a736aac89e5025d49a8ca10b01bb1a81cd6ce7/polyfills/globalself.js). 62 | -------------------------------------------------------------------------------- /docs/guides/request-deduplication.md: -------------------------------------------------------------------------------- 1 | # Request Deduplication 2 | 3 | React Request will prevent two identical HTTP requests from being in flight at the 4 | same time. It does this by comparing the [request key](./request-keys.md) of any 5 | new request with the keys of all in-flight requests. 6 | 7 | When an existing request is already in flight for that same key, then a new 8 | request will not be dispatched. Instead, the same response will be sent to 9 | both requestors. 10 | 11 | ### Examples 12 | 13 | In the following example, only one HTTP request is made: 14 | 15 | ```jsx 16 | class App extends Component { 17 | render() { 18 | return ( 19 |
20 | 21 | 22 |
23 | ); 24 | } 25 | } 26 | ``` 27 | 28 | However, in this example, two requests are made because the URLs are different, resulting in 29 | a different request key: 30 | 31 | ```jsx 32 | class App extends Component { 33 | render() { 34 | return ( 35 |
36 | 37 | 38 |
39 | ); 40 | } 41 | } 42 | ``` 43 | 44 | ### Reliability 45 | 46 | The default request key implementation assumes that `JSON.stringify` will produce the same string given two different objects 47 | that would be considered "deeply equal" (for instance, if they were compared using [`_.isEqual`](https://lodash.com/docs/#isEqual)). 48 | 49 | This may seem unreliable to you, but Apollo 50 | [has been doing it this way for some time](https://github.com/apollographql/apollo-link/blob/d5b0d4c491563ed36c50170e0b4c6c5f8c988d59/packages/apollo-link/src/linkUtils.ts#L121-L127), 51 | and that is a library with half a million downloads per month (as of February 2018). So it seems to 52 | be reliable. 53 | 54 | Needless to say, if this behavior ever causes problems, then we will revisit the approach. 55 | 56 | ### Disabling deduplication 57 | 58 | You can disable deduplication with the `dedupe` prop. 59 | 60 | ```jsx 61 | // In this example, two identical HTTP requests will be made at the same time. 62 | class App extends Component { 63 | render() { 64 | return ( 65 |
66 | 67 | 68 |
69 | ); 70 | } 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /docs/guides/request-keys.md: -------------------------------------------------------------------------------- 1 | # Request Keys 2 | 3 | React Request's two most useful features are 4 | [request deduplication](./request-deduplication.md) and 5 | [response caching](./response-caching.md). 6 | 7 | Both of these features are powered by a system called "request keys," which are strings that 8 | allow React Request to determine if two requests are to be considered identical. 9 | 10 | ### What is a Request Key 11 | 12 | A request key is a string that is created from the props that you pass to `Fetch`. 13 | 14 | The key is composed of these pieces of information: 15 | 16 | * `url` 17 | * `method` 18 | * `body` 19 | 20 | ### How is it Used 21 | 22 | **Request Deduplication** 23 | 24 | When two or more `` components with the same key attempt to fire off a request, only one HTTP 25 | request will be sent off. When the response is returned, all of the components will 26 | receive that single response. 27 | 28 | For more, see the guide on [request deduplication](./request-deduplication.md). 29 | 30 | **Response Caching** 31 | 32 | Anytime a response is received for a request, it is stored in an internal cache. If 33 | a subsequent request is attempted with the same key, then the cached version 34 | will be returned instead. 35 | 36 | For more, see the guide on [response caching](./response-caching.md). 37 | -------------------------------------------------------------------------------- /docs/guides/response-caching.md: -------------------------------------------------------------------------------- 1 | # Response Caching 2 | 3 | React Request has a built-in response caching system. Interactions with the 4 | cache are configurable with the `fetchPolicy` and `cacheResponse` prop. 5 | 6 | The way the cache works is like this: any time a response from the server is received, 7 | it will be cached using the request's [request key](./request-keys.md). Subsequent 8 | requests are matched with existing cached server responses using _their_ request key. 9 | 10 | The `cacheResponse` prop determines if server responses will be cached. Typically, 11 | you only want to cache responses for "read" requests. Accordingly, the default 12 | value is based on the value of the `method` prop: 13 | 14 | | Method | Default value | 15 | | ------------------------ | ------------- | 16 | | GET, HEAD, OPTIONS | `true` | 17 | | POST, PUT, PATCH, DELETE | `false` | 18 | 19 | There are four ways that a `` component can interact with the 20 | cached responses, which are configurable with the `fetchPolicy` prop: 21 | 22 | ### `cache-first` 23 | 24 | This is the default behavior. 25 | 26 | Requests will first look at the cache to see if a response for the same key exists. If a response is 27 | found, then it will be returned, and no network request will be made. 28 | 29 | If no response exists in the cache, then a network request will be made. 30 | 31 | ### `cache-and-network` 32 | 33 | Requests will first look at the cache. If a response exists in the cache, 34 | then it will immediately be returned. 35 | 36 | Whether or not a response exists in the cache, a network request will be made. 37 | 38 | ### `network-only` 39 | 40 | The cache is ignored, and a network request is always made. 41 | 42 | ### `cache-only` 43 | 44 | If a response exists in the cache, then it will be returned. If no response 45 | exists in the cache, then an error will be passed into the render prop function. 46 | 47 | --- 48 | 49 | Like `cacheResponse`, the default value of `fetchPolicy` is based on the method that you pass. 50 | 51 | | Method | Default value | 52 | | ------------------------ | ---------------- | 53 | | GET, HEAD, OPTIONS | `"cache-first"` | 54 | | POST, PUT, PATCH, DELETE | `"network-only"` | 55 | 56 | ### Using `POST` for read requests 57 | 58 | Some APIs use the `POST` method for read requests. React Request supports this, but you will 59 | need to manually configure the cache. This may look something like this: 60 | 61 | ```jsx 62 | 69 | ``` 70 | 71 | With the above configuration, responses will be stored in the cache, and requests will 72 | only be made when the cache is empty. Also note that the [lazy prop](./using-the-lazy-prop.md) 73 | is set to `false` so that the request fires when the component mounts. 74 | -------------------------------------------------------------------------------- /docs/guides/using-the-lazy-prop.md: -------------------------------------------------------------------------------- 1 | # Using the `lazy` Prop 2 | 3 | One of the props of the `` component is `lazy`. This 4 | determines whether or not a request will be made when the 5 | component mounts. 6 | 7 | The default value of `lazy` depends on the request method 8 | being used. 9 | 10 | | Method | Default value | 11 | | ------------------------ | ------------- | 12 | | GET, HEAD, OPTIONS | `false` | 13 | | POST, PUT, PATCH, DELETE | `true` | 14 | 15 | This is due to the way applications typically use these methods 16 | when performing reads and writes. 17 | 18 | ### Read Requests 19 | 20 | Read requests are frequently done as a result of the user navigating 21 | to some section of the page. For instance, if your application is for 22 | a library, and the user visits the URL `/books/2`, then you will likely 23 | want to kick off a request to fetch that book right after the page loads. 24 | 25 | This is why `lazy` is `false` for `GET` requests. 26 | 27 | > Sometimes, APIs will use `POST` for read requests. In these situations, you will 28 | need to manually specify `lazy` as `false` if you would like to make the request 29 | when the component mounts. 30 | 31 | ### Write Requests 32 | 33 | Typically, write requests, such as updating or deleting a resource, are not done 34 | as the result of a user simply navigating to a page. Instead, these are typically 35 | performed when a user clicks a button to confirm the action. 36 | 37 | This is why `lazy` is `true` for HTTP methods that typically refer to write requests. 38 | 39 | ### Dynamic `lazy` prop usage 40 | 41 | A neat pattern is to specify a dynamic `lazy` value based on some application state. For 42 | instance, consider a search page that serializes the user's search into a query parameter. 43 | 44 | When the app loads, you may want to make the request immediately when the query parameter 45 | exists. But if there isn't a query parameter, then you won't want to make the request, because 46 | there's no search term to use in the request. 47 | 48 | You can use a dynamic value for `lazy` to implement this behavior. The example below 49 | demonstrates how you might go about doing this. 50 | 51 | ```jsx 52 | 53 | {children} 54 | 55 | ``` 56 | -------------------------------------------------------------------------------- /examples/fetch-components/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/fetch-components/README.md: -------------------------------------------------------------------------------- 1 | # Fetch Components 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | 27 | ### `npm run eject` 28 | 29 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 30 | 31 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 32 | 33 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 34 | 35 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 36 | -------------------------------------------------------------------------------- /examples/fetch-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetch-components", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.2.0", 7 | "react-composer": "^4.1.0", 8 | "react-dom": "^16.2.0", 9 | "react-request": "^3.0.0", 10 | "react-scripts": "1.1.0" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test --env=jsdom", 16 | "eject": "react-scripts eject" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/fetch-components/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesplease/react-request/efda7bdcc083b9b956203ce6ef5eefadc6b36aa5/examples/fetch-components/public/favicon.ico -------------------------------------------------------------------------------- /examples/fetch-components/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/fetch-components/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/fetch-components/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Composer from 'react-composer'; 3 | import { ReadPost, UpdatePost, DeletePost } from './fetch-components/posts'; 4 | 5 | class App extends Component { 6 | render() { 7 | // This ID could come from, say, props 8 | const postId = '1'; 9 | 10 | return ( 11 | , 14 | , 15 | , 16 | ]}> 17 | {([readPost, updatePost, deletePost]) => ( 18 |
19 |
20 | 29 | 44 |
45 |
46 | {readPost.fetching && 'Loading...'} 47 | {readPost.failed && 'There was some kind of error'} 48 | {readPost.data && ( 49 |
50 |

Post title: {readPost.data.title}

51 |

Post ID: {readPost.data.id}

52 |
53 | )} 54 |
55 |
56 | )} 57 |
58 | ); 59 | } 60 | } 61 | 62 | export default App; 63 | -------------------------------------------------------------------------------- /examples/fetch-components/src/fetch-components/posts.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Fetch } from 'react-request'; 3 | 4 | // The idea behind this is to abstract away your HTTP request configuration. 5 | // Typically, you don't want things like URLs and header configuration scattered 6 | // throughout your app. 7 | // 8 | // This allows you to reuse all of that HTTP configuration in your app by 9 | // passing the minimum amount of information necessary to make the request. 10 | // 11 | // In the case of this simple "post" resource, all that is needed is an ID (and 12 | // children, since Fetch is a render prop component) 13 | 14 | export function ReadPost({ postId, children }) { 15 | return ( 16 | 20 | ); 21 | } 22 | 23 | export function UpdatePost({ postId, children }) { 24 | return ( 25 | 30 | ); 31 | } 32 | 33 | export function DeletePost({ postId, children }) { 34 | return ( 35 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /examples/fetch-components/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /examples/lazy-read/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/lazy-read/README.md: -------------------------------------------------------------------------------- 1 | # Lazy Read 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | 27 | ### `npm run eject` 28 | 29 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 30 | 31 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 32 | 33 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 34 | 35 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 36 | -------------------------------------------------------------------------------- /examples/lazy-read/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lazy-read", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.2.0", 7 | "react-dom": "^16.2.0", 8 | "react-request": "^3.0.0", 9 | "react-scripts": "1.1.0" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test --env=jsdom", 15 | "eject": "react-scripts eject" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/lazy-read/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesplease/react-request/efda7bdcc083b9b956203ce6ef5eefadc6b36aa5/examples/lazy-read/public/favicon.ico -------------------------------------------------------------------------------- /examples/lazy-read/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/lazy-read/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/lazy-read/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Fetch } from 'react-request'; 3 | 4 | class App extends Component { 5 | render() { 6 | return ( 7 | 8 | {({ fetching, failed, data, doFetch }) => ( 9 |
10 | 13 | {fetching && 'Loading...'} 14 | {failed && 'There was some kind of error'} 15 | {data && ( 16 |
17 |

Post title: {data.title}

18 |

Post ID: {data.id}

19 |
20 | )} 21 |
22 | )} 23 |
24 | ); 25 | } 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /examples/lazy-read/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /examples/multiple-requests/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/multiple-requests/README.md: -------------------------------------------------------------------------------- 1 | # Multiple Requests 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | 27 | ### `npm run eject` 28 | 29 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 30 | 31 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 32 | 33 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 34 | 35 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 36 | -------------------------------------------------------------------------------- /examples/multiple-requests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multiple-requests", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.2.0", 7 | "react-composer": "^4.1.0", 8 | "react-dom": "^16.2.0", 9 | "react-request": "^3.0.0", 10 | "react-scripts": "1.1.0" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test --env=jsdom", 16 | "eject": "react-scripts eject" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/multiple-requests/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesplease/react-request/efda7bdcc083b9b956203ce6ef5eefadc6b36aa5/examples/multiple-requests/public/favicon.ico -------------------------------------------------------------------------------- /examples/multiple-requests/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/multiple-requests/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/multiple-requests/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Composer from 'react-composer'; 3 | import { Fetch } from 'react-request'; 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 | , 11 | , 12 | , 16 | ]}> 17 | {([readPostOne, readPostTwo, deletePostOne]) => ( 18 |
19 | 24 | 25 | 26 | {readPostOne.fetching && 'Fetching post 1...'} 27 | {!readPostOne.fetching && 'Not currently fetching post 1.'} 28 | {readPostTwo.fetching && 'Fetching post 2...'} 29 | {!readPostTwo.fetching && 'Not currently fetching post 2.'} 30 |
31 | )} 32 |
33 | ); 34 | } 35 | } 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /examples/multiple-requests/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /examples/request-deduplication/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/request-deduplication/README.md: -------------------------------------------------------------------------------- 1 | # Request Deduplication 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | 27 | ### `npm run eject` 28 | 29 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 30 | 31 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 32 | 33 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 34 | 35 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 36 | -------------------------------------------------------------------------------- /examples/request-deduplication/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "request-deduplication", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.2.0", 7 | "react-dom": "^16.2.0", 8 | "react-request": "^3.0.0", 9 | "react-scripts": "1.1.0" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test --env=jsdom", 15 | "eject": "react-scripts eject" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/request-deduplication/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesplease/react-request/efda7bdcc083b9b956203ce6ef5eefadc6b36aa5/examples/request-deduplication/public/favicon.ico -------------------------------------------------------------------------------- /examples/request-deduplication/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/request-deduplication/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/request-deduplication/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Fetch } from 'react-request'; 3 | 4 | const urlOne = 'https://jsonplaceholder.typicode.com/posts/1'; 5 | const urlTwo = 'https://jsonplaceholder.typicode.com/posts/2'; 6 | 7 | class App extends Component { 8 | render() { 9 | // Open DevTools to observe that only one request is made for each URL 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | Open the Network tab within DevTools to see the number of requests 26 | that are sent off. 27 |
28 |
29 | ); 30 | } 31 | } 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /examples/request-deduplication/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /examples/response-caching/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/response-caching/README.md: -------------------------------------------------------------------------------- 1 | # Response Caching 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | 27 | ### `npm run eject` 28 | 29 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 30 | 31 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 32 | 33 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 34 | 35 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 36 | -------------------------------------------------------------------------------- /examples/response-caching/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "response-caching", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.2.0", 7 | "react-dom": "^16.2.0", 8 | "react-request": "^3.0.0", 9 | "react-scripts": "1.1.0" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test --env=jsdom", 15 | "eject": "react-scripts eject" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/response-caching/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesplease/react-request/efda7bdcc083b9b956203ce6ef5eefadc6b36aa5/examples/response-caching/public/favicon.ico -------------------------------------------------------------------------------- /examples/response-caching/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/response-caching/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/response-caching/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { Fetch } from 'react-request'; 3 | import { setTimeout } from 'timers'; 4 | 5 | const url = 'https://jsonplaceholder.typicode.com/posts/1'; 6 | let renderCount = 0; 7 | 8 | class App extends Component { 9 | render() { 10 | // Open DevTools to observe that only one request is made, even though two fetch components are mounted. 11 | // The second component receives the data that was cached after the first Fetch's response completes. 12 | return ( 13 |
14 | 15 | {this.state.fetchAgain && ( 16 | 17 | {stuff => { 18 | if (renderCount === 0) { 19 | console.log( 20 | 'The second fetch component just mounted. You should see that only one request was made in the network tab (unless your connection is really slow).', 21 | stuff 22 | ); 23 | 24 | renderCount++; 25 | } 26 | 27 | return null; 28 | }} 29 | 30 | )} 31 |
Check out the DevTools console to interpret this example.
32 |
33 | ); 34 | } 35 | 36 | state = { 37 | fetchAgain: false, 38 | }; 39 | 40 | componentDidMount() { 41 | setTimeout(() => { 42 | this.setState({ 43 | fetchAgain: true, 44 | }); 45 | }, 2000); 46 | } 47 | } 48 | 49 | export default App; 50 | -------------------------------------------------------------------------------- /examples/response-caching/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /examples/simple-read/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/simple-read/README.md: -------------------------------------------------------------------------------- 1 | # Simple Read 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | 27 | ### `npm run eject` 28 | 29 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 30 | 31 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 32 | 33 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 34 | 35 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 36 | -------------------------------------------------------------------------------- /examples/simple-read/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-read", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.2.0", 7 | "react-dom": "^16.2.0", 8 | "react-request": "^3.0.0", 9 | "react-scripts": "1.1.0" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test --env=jsdom", 15 | "eject": "react-scripts eject" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/simple-read/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesplease/react-request/efda7bdcc083b9b956203ce6ef5eefadc6b36aa5/examples/simple-read/public/favicon.ico -------------------------------------------------------------------------------- /examples/simple-read/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/simple-read/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/simple-read/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Fetch } from 'react-request'; 3 | 4 | class App extends Component { 5 | render() { 6 | return ( 7 | 8 | {({ fetching, failed, data }) => ( 9 |
10 | {fetching && 'Loading...'} 11 | {failed && 'There was some kind of error'} 12 | {data && ( 13 |
14 |

Post title: {data.title}

15 |

Post ID: {data.id}

16 |
17 | )} 18 |
19 | )} 20 |
21 | ); 22 | } 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /examples/simple-read/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /examples/updating-a-resource/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/updating-a-resource/README.md: -------------------------------------------------------------------------------- 1 | # Updating a Resource 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | 27 | ### `npm run eject` 28 | 29 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 30 | 31 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 32 | 33 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 34 | 35 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 36 | -------------------------------------------------------------------------------- /examples/updating-a-resource/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "updating-a-resource", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.2.0", 7 | "react-dom": "^16.2.0", 8 | "react-request": "^3.0.0", 9 | "react-scripts": "1.1.0" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test --env=jsdom", 15 | "eject": "react-scripts eject" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/updating-a-resource/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesplease/react-request/efda7bdcc083b9b956203ce6ef5eefadc6b36aa5/examples/updating-a-resource/public/favicon.ico -------------------------------------------------------------------------------- /examples/updating-a-resource/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/updating-a-resource/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/updating-a-resource/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Fetch } from 'react-request'; 3 | 4 | class App extends Component { 5 | render() { 6 | return ( 7 | 8 | {({ fetching, failed, doFetch }) => ( 9 |
10 | 25 | {fetching && 'Saving post 1...'} 26 | {failed && 'There was some kind of error'} 27 |
28 | )} 29 |
30 | ); 31 | } 32 | 33 | getUpdatedPost() { 34 | return JSON.stringify({ 35 | title: 'hello', 36 | }); 37 | } 38 | } 39 | 40 | export default App; 41 | -------------------------------------------------------------------------------- /examples/updating-a-resource/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | collectCoverageFrom: ['src/**/*.{js,jsx}', '!**/node_modules/**'], 4 | coverageDirectory: 'coverage', 5 | setupTestFrameworkScriptFile: './test/setup.js', 6 | testURL: 'http://localhost/', 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-request", 3 | "version": "3.2.0", 4 | "description": "Declarative HTTP requests with React.", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "scripts": { 8 | "clean": "rimraf dist es tmp lib", 9 | "test": "npm run lint && npm run test:unit", 10 | "test:unit": "jest", 11 | "lint": "eslint src test", 12 | "prepublish": "in-publish && npm run build || not-in-publish", 13 | "build": "npm run clean && npm run build:umd && npm run build:umd:min && npm run build:es && npm run build:commonjs", 14 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 15 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 16 | "build:umd": "cross-env NODE_ENV=development BABEL_ENV=build rollup -c -i src/index.js -o dist/react-request.js", 17 | "build:umd:min": "cross-env NODE_ENV=production BABEL_ENV=buildProd rollup -c -i src/index.js -o dist/react-request.min.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/jamesplease/react-request.git" 22 | }, 23 | "keywords": [ 24 | "react", 25 | "http", 26 | "https", 27 | "request", 28 | "requests", 29 | "response", 30 | "xhr", 31 | "xmlhttprequest", 32 | "fetch", 33 | "cors", 34 | "json", 35 | "api", 36 | "data", 37 | "rest", 38 | "restful", 39 | "crud" 40 | ], 41 | "author": "James Smith ", 42 | "license": "MIT", 43 | "bugs": { 44 | "url": "https://github.com/jamesplease/react-request/issues" 45 | }, 46 | "files": [ 47 | "dist", 48 | "lib", 49 | "es" 50 | ], 51 | "peerDependencies": { 52 | "react": "^15.0.0 || ^16.0.0 || ^17.0.0" 53 | }, 54 | "devDependencies": { 55 | "babel-cli": "^6.26.0", 56 | "babel-core": "^6.26.0", 57 | "babel-eslint": "^8.2.1", 58 | "babel-jest": "^22.1.0", 59 | "babel-plugin-external-helpers": "^6.22.0", 60 | "babel-plugin-transform-class-properties": "^6.24.1", 61 | "babel-plugin-transform-react-remove-prop-types": "^0.4.12", 62 | "babel-preset-env": "^1.6.1", 63 | "babel-preset-react": "^6.24.1", 64 | "babel-preset-stage-3": "^6.24.1", 65 | "coveralls": "^3.0.0", 66 | "cross-env": "^5.1.3", 67 | "enzyme": "^3.3.0", 68 | "enzyme-adapter-react-16": "^1.1.1", 69 | "eslint": "^4.17.0", 70 | "eslint-plugin-react": "^7.6.1", 71 | "fetch-mock": "^6.0.0", 72 | "in-publish": "^2.0.0", 73 | "isomorphic-fetch": "^2.2.1", 74 | "jest": "^22.1.4", 75 | "react": "^16.2.0", 76 | "react-dom": "^16.2.0", 77 | "react-test-renderer": "^16.2.0", 78 | "rimraf": "^2.6.2", 79 | "rollup": "^0.45.1", 80 | "rollup-plugin-babel": "^2.7.1", 81 | "rollup-plugin-commonjs": "^8.2.6", 82 | "rollup-plugin-node-resolve": "^3.0.0", 83 | "rollup-plugin-replace": "^1.2.1", 84 | "rollup-plugin-uglify": "^2.0.1" 85 | }, 86 | "dependencies": { 87 | "fetch-dedupe": "^3.0.0", 88 | "prop-types": "^15.6.0" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import babel from 'rollup-plugin-babel'; 4 | import uglify from 'rollup-plugin-uglify'; 5 | import replace from 'rollup-plugin-replace'; 6 | 7 | var env = process.env.NODE_ENV; 8 | var config = { 9 | format: 'umd', 10 | moduleName: 'ReactRequest', 11 | external: ['react'], 12 | globals: { 13 | react: 'React', 14 | }, 15 | context: 'this', 16 | plugins: [ 17 | nodeResolve({ 18 | jsnext: true, 19 | }), 20 | commonjs({ 21 | include: 'node_modules/**', 22 | 23 | // explicitly specify unresolvable named exports 24 | // (see below for more details) 25 | // namedExports: { './module.js': ['foo', 'bar' ] }, // Default: undefined 26 | }), 27 | babel({ 28 | exclude: 'node_modules/**', 29 | }), 30 | replace({ 31 | 'process.env.NODE_ENV': JSON.stringify(env), 32 | }), 33 | ], 34 | }; 35 | 36 | if (env === 'production') { 37 | config.plugins.push( 38 | uglify({ 39 | compress: { 40 | pure_getters: true, 41 | unsafe: true, 42 | unsafe_comps: true, 43 | }, 44 | }) 45 | ); 46 | } 47 | 48 | export default config; 49 | -------------------------------------------------------------------------------- /src/fetch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { getRequestKey, fetchDedupe, isRequestInFlight } from 'fetch-dedupe'; 4 | 5 | // This object is our cache 6 | // The keys of the object are requestKeys 7 | // The value of each key is a Response instance 8 | let responseCache = {}; 9 | 10 | // The docs state that this is not safe to use in an 11 | // application. That's just because I am not writing tests, 12 | // nor designing the API, around folks clearing the cache. 13 | // This was only added to help out with testing your app. 14 | // Use your judgment if you decide to use this in your 15 | // app directly. 16 | export function clearResponseCache() { 17 | responseCache = {}; 18 | } 19 | 20 | export class Fetch extends React.Component { 21 | render() { 22 | // Anything pulled from `this.props` here is not eligible to be 23 | // specified when calling `doFetch`. 24 | const { children, requestName } = this.props; 25 | const { fetching, response, data, error, requestKey, url } = this.state; 26 | 27 | if (!children) { 28 | return null; 29 | } else { 30 | return ( 31 | children({ 32 | requestName, 33 | url, 34 | fetching, 35 | failed: Boolean(error || (response && !response.ok)), 36 | response, 37 | data, 38 | requestKey, 39 | error, 40 | doFetch: this.fetchRenderProp, 41 | }) || null 42 | ); 43 | } 44 | } 45 | 46 | constructor(props, context) { 47 | super(props, context); 48 | 49 | this.state = { 50 | requestKey: 51 | props.requestKey || 52 | getRequestKey({ 53 | ...props, 54 | method: props.method.toUpperCase(), 55 | }), 56 | requestName: props.requestName, 57 | fetching: false, 58 | response: null, 59 | data: null, 60 | error: null, 61 | url: props.url, 62 | }; 63 | } 64 | 65 | isReadRequest = method => { 66 | const uppercaseMethod = method.toUpperCase(); 67 | 68 | return ( 69 | uppercaseMethod === 'GET' || 70 | uppercaseMethod === 'HEAD' || 71 | uppercaseMethod === 'OPTIONS' 72 | ); 73 | }; 74 | 75 | // We default to being lazy for "write" requests, 76 | // such as POST, PATCH, DELETE, and so on. 77 | isLazy = () => { 78 | const { lazy, method } = this.props; 79 | 80 | return typeof lazy === 'undefined' ? !this.isReadRequest(method) : lazy; 81 | }; 82 | 83 | shouldCacheResponse = () => { 84 | const { cacheResponse, method } = this.props; 85 | 86 | return typeof cacheResponse === 'undefined' 87 | ? this.isReadRequest(method) 88 | : cacheResponse; 89 | }; 90 | 91 | getFetchPolicy = () => { 92 | const { fetchPolicy, method } = this.props; 93 | 94 | if (typeof fetchPolicy === 'undefined') { 95 | return this.isReadRequest(method) ? 'cache-first' : 'network-only'; 96 | } else { 97 | return fetchPolicy; 98 | } 99 | }; 100 | 101 | componentDidMount() { 102 | if (!this.isLazy()) { 103 | this.fetchData(); 104 | } 105 | } 106 | 107 | // Because we use `componentDidUpdate` to determine if we should fetch 108 | // again, there will be at least one render when you receive your new 109 | // fetch options, such as a new URL, but the fetch has not begun yet. 110 | componentDidUpdate(prevProps) { 111 | const currentRequestKey = 112 | this.props.requestKey || 113 | getRequestKey({ 114 | ...this.props, 115 | method: this.props.method.toUpperCase(), 116 | }); 117 | const prevRequestKey = 118 | prevProps.requestKey || 119 | getRequestKey({ 120 | ...prevProps, 121 | method: prevProps.method.toUpperCase(), 122 | }); 123 | 124 | if (currentRequestKey !== prevRequestKey && !this.isLazy()) { 125 | this.fetchData({ 126 | requestKey: currentRequestKey, 127 | }); 128 | } 129 | } 130 | 131 | componentWillUnmount() { 132 | this.willUnmount = true; 133 | this.cancelExistingRequest('Component unmounted'); 134 | } 135 | 136 | // When a request is already in flight, and a new one is 137 | // configured, then we need to "cancel" the previous one. 138 | cancelExistingRequest = reason => { 139 | if (this.state.fetching && this._currentRequestKey !== null) { 140 | const abortError = new Error(reason); 141 | // This is an effort to mimic the error that is created when a 142 | // fetch is actually aborted using the AbortController API. 143 | abortError.name = 'AbortError'; 144 | this.onResponseReceived({ 145 | ...this.responseReceivedInfo, 146 | error: abortError, 147 | hittingNetwork: true, 148 | }); 149 | } 150 | }; 151 | 152 | fetchRenderProp = options => { 153 | return new Promise(resolve => { 154 | // We wrap this in a setTimeout so as to avoid calls to `setState` 155 | // in render, which React does not allow. 156 | // 157 | // tl;dr, the following code should never cause a React warning or error: 158 | // 159 | // ` doFetch()} /> 160 | setTimeout(() => { 161 | this.fetchData(options, true, resolve); 162 | }); 163 | }); 164 | }; 165 | 166 | // When a subsequent request is made, it is important that the correct 167 | // request key is used. This method computes the right key based on the 168 | // options and props. 169 | getRequestKey = options => { 170 | // A request key in the options gets top priority 171 | if (options && options.requestKey) { 172 | return options.requestKey; 173 | } 174 | 175 | // Otherwise, if we have no request key, but we do have options, then we 176 | // recompute the request key based on these options. 177 | // Note that if the URL, body, or method have not changed, then the request 178 | // key should match the previous request key if it was computed. 179 | // If you passed in a custom request key as a prop, then you will also 180 | // need to pass in a custom key when you call `doFetch()`! 181 | else if (options) { 182 | const { url, method, body } = Object.assign({}, this.props, options); 183 | return getRequestKey({ 184 | url, 185 | body, 186 | method: method.toUpperCase(), 187 | }); 188 | } 189 | 190 | // Next in line is the the request key from props. 191 | else if (this.props.requestKey) { 192 | return this.props.requestKey; 193 | } 194 | 195 | // Lastly, we compute the request key from the props. 196 | else { 197 | const { url, method, body } = this.props; 198 | 199 | return getRequestKey({ 200 | url, 201 | body, 202 | method: method.toUpperCase(), 203 | }); 204 | } 205 | }; 206 | 207 | fetchData = (options, ignoreCache, resolve) => { 208 | // These are the things that we do not allow a user to configure in 209 | // `options` when calling `doFetch()`. Perhaps we should, however. 210 | const { requestName, dedupe, beforeFetch } = this.props; 211 | 212 | this.cancelExistingRequest('New fetch initiated'); 213 | 214 | const requestKey = this.getRequestKey(options); 215 | const requestOptions = Object.assign({}, this.props, options); 216 | 217 | this._currentRequestKey = requestKey; 218 | 219 | const { 220 | url, 221 | body, 222 | credentials, 223 | headers, 224 | method, 225 | responseType, 226 | mode, 227 | cache, 228 | redirect, 229 | referrer, 230 | referrerPolicy, 231 | integrity, 232 | keepalive, 233 | signal, 234 | } = requestOptions; 235 | 236 | const uppercaseMethod = method.toUpperCase(); 237 | const shouldCacheResponse = this.shouldCacheResponse(); 238 | 239 | const init = { 240 | body, 241 | credentials, 242 | headers, 243 | method: uppercaseMethod, 244 | mode, 245 | cache, 246 | redirect, 247 | referrer, 248 | referrerPolicy, 249 | integrity, 250 | keepalive, 251 | signal, 252 | }; 253 | 254 | const responseReceivedInfo = { 255 | url, 256 | init, 257 | requestKey, 258 | responseType, 259 | }; 260 | 261 | // This is necessary because `options` may have overridden the props. 262 | // If the request config changes, we need to be able to accurately 263 | // cancel the in-flight request. 264 | this.responseReceivedInfo = responseReceivedInfo; 265 | 266 | const fetchPolicy = this.getFetchPolicy(); 267 | 268 | let cachedResponse; 269 | if (fetchPolicy !== 'network-only' && !ignoreCache) { 270 | cachedResponse = responseCache[requestKey]; 271 | 272 | if (cachedResponse) { 273 | this.onResponseReceived({ 274 | ...responseReceivedInfo, 275 | response: cachedResponse, 276 | hittingNetwork: false, 277 | stillFetching: fetchPolicy === 'cache-and-network', 278 | }); 279 | 280 | if (fetchPolicy === 'cache-first' || fetchPolicy === 'cache-only') { 281 | return Promise.resolve(cachedResponse); 282 | } 283 | } else if (fetchPolicy === 'cache-only') { 284 | const cacheError = new Error( 285 | `Response for "${requestName}" not found in cache.` 286 | ); 287 | this.onResponseReceived({ 288 | ...responseReceivedInfo, 289 | error: cacheError, 290 | hittingNetwork: false, 291 | }); 292 | return Promise.resolve(cacheError); 293 | } 294 | } 295 | 296 | this.setState({ 297 | requestKey, 298 | url, 299 | error: null, 300 | failed: false, 301 | fetching: true, 302 | }); 303 | const hittingNetwork = !isRequestInFlight(requestKey) || !dedupe; 304 | 305 | if (hittingNetwork) { 306 | beforeFetch({ 307 | url, 308 | init, 309 | requestKey, 310 | }); 311 | } 312 | return fetchDedupe(url, init, { requestKey, responseType, dedupe }).then( 313 | res => { 314 | if (shouldCacheResponse) { 315 | responseCache[requestKey] = res; 316 | } 317 | 318 | if (this._currentRequestKey === requestKey) { 319 | this.onResponseReceived({ 320 | ...responseReceivedInfo, 321 | response: res, 322 | hittingNetwork, 323 | resolve, 324 | }); 325 | } 326 | 327 | return res; 328 | }, 329 | error => { 330 | if (this._currentRequestKey === requestKey) { 331 | this.onResponseReceived({ 332 | ...responseReceivedInfo, 333 | error, 334 | cachedResponse, 335 | hittingNetwork, 336 | resolve, 337 | }); 338 | } 339 | 340 | return error; 341 | } 342 | ); 343 | }; 344 | 345 | onResponseReceived = info => { 346 | const { 347 | error = null, 348 | response = null, 349 | hittingNetwork, 350 | url, 351 | init, 352 | requestKey, 353 | cachedResponse, 354 | stillFetching = false, 355 | resolve, 356 | } = info; 357 | 358 | this.responseReceivedInfo = null; 359 | 360 | if (!stillFetching) { 361 | this._currentRequestKey = null; 362 | } 363 | 364 | let data; 365 | // If our response succeeded, then we use that data. 366 | if (response && response.data) { 367 | data = response.data; 368 | } else if (cachedResponse && cachedResponse.data) { 369 | // This happens when the request failed, but we have cache-and-network 370 | // specified. Although we pass along the failed response, we continue to 371 | // pass in the cached data. 372 | data = cachedResponse.data; 373 | } 374 | 375 | data = data ? this.props.transformData(data) : null; 376 | 377 | // If we already have some data in state on error, then we continue to 378 | // pass that data down. This prevents the data from being wiped when a 379 | // request fails, which is generally not what people want. 380 | // For more, see: GitHub Issue #154 381 | if (error && this.state.data) { 382 | data = this.state.data; 383 | } 384 | 385 | const afterFetchInfo = { 386 | url, 387 | init, 388 | requestKey, 389 | error, 390 | failed: Boolean(error || (response && !response.ok)), 391 | response, 392 | data, 393 | didUnmount: Boolean(this.willUnmount), 394 | }; 395 | 396 | if (typeof resolve === 'function') { 397 | resolve(afterFetchInfo); 398 | } 399 | 400 | if (hittingNetwork) { 401 | this.props.afterFetch(afterFetchInfo); 402 | } 403 | 404 | if (this.willUnmount) { 405 | return; 406 | } 407 | 408 | this.setState( 409 | { 410 | url, 411 | data, 412 | error, 413 | response, 414 | fetching: stillFetching, 415 | requestKey, 416 | }, 417 | () => this.props.onResponse(error, response) 418 | ); 419 | }; 420 | } 421 | 422 | const globalObj = typeof self !== 'undefined' ? self : this; 423 | const AbortSignalCtr = 424 | globalObj !== undefined ? globalObj.AbortSignal : function() {}; 425 | 426 | Fetch.propTypes = { 427 | children: PropTypes.func, 428 | requestName: PropTypes.string, 429 | fetchPolicy: PropTypes.oneOf([ 430 | 'cache-first', 431 | 'cache-and-network', 432 | 'network-only', 433 | 'cache-only', 434 | ]), 435 | onResponse: PropTypes.func, 436 | beforeFetch: PropTypes.func, 437 | afterFetch: PropTypes.func, 438 | responseType: PropTypes.oneOfType([ 439 | PropTypes.func, 440 | PropTypes.oneOf(['json', 'text', 'blob', 'arrayBuffer', 'formData']), 441 | ]), 442 | transformData: PropTypes.func, 443 | lazy: PropTypes.bool, 444 | dedupe: PropTypes.bool, 445 | requestKey: PropTypes.string, 446 | 447 | url: PropTypes.string.isRequired, 448 | body: PropTypes.any, 449 | credentials: PropTypes.oneOf(['omit', 'same-origin', 'include']), 450 | headers: PropTypes.object, 451 | method: PropTypes.oneOf([ 452 | 'get', 453 | 'post', 454 | 'put', 455 | 'patch', 456 | 'delete', 457 | 'options', 458 | 'head', 459 | 'GET', 460 | 'POST', 461 | 'PUT', 462 | 'PATCH', 463 | 'DELETE', 464 | 'OPTIONS', 465 | 'HEAD', 466 | ]), 467 | mode: PropTypes.oneOf([ 468 | 'same-origin', 469 | 'cors', 470 | 'no-cors', 471 | 'navigate', 472 | 'websocket', 473 | ]), 474 | cache: PropTypes.oneOf([ 475 | 'default', 476 | 'no-store', 477 | 'reload', 478 | 'no-cache', 479 | 'force-cache', 480 | 'only-if-cached', 481 | ]), 482 | redirect: PropTypes.oneOf(['manual', 'follow', 'error']), 483 | referrer: PropTypes.string, 484 | referrerPolicy: PropTypes.oneOf([ 485 | 'no-referrer', 486 | 'no-referrer-when-downgrade', 487 | 'origin', 488 | 'origin-when-cross-origin', 489 | 'unsafe-url', 490 | '', 491 | ]), 492 | integrity: PropTypes.string, 493 | keepalive: PropTypes.bool, 494 | signal: PropTypes.instanceOf(AbortSignalCtr), 495 | }; 496 | 497 | Fetch.defaultProps = { 498 | requestName: 'anonymousRequest', 499 | onResponse: () => {}, 500 | beforeFetch: () => {}, 501 | afterFetch: () => {}, 502 | transformData: data => data, 503 | dedupe: true, 504 | 505 | method: 'get', 506 | referrerPolicy: '', 507 | integrity: '', 508 | referrer: 'about:client', 509 | }; 510 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { Fetch, clearResponseCache } from './fetch'; 2 | import { 3 | fetchDedupe, 4 | getRequestKey, 5 | clearRequestCache, 6 | isRequestInFlight, 7 | } from 'fetch-dedupe'; 8 | 9 | export { 10 | Fetch, 11 | fetchDedupe, 12 | getRequestKey, 13 | isRequestInFlight, 14 | clearRequestCache, 15 | clearResponseCache, 16 | }; 17 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../.eslintrc" 4 | ], 5 | "env": { 6 | "jest": true, 7 | "node": true 8 | }, 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | "jsx": true 12 | } 13 | }, 14 | "globals": { 15 | "hangingPromise": true 16 | } 17 | } -------------------------------------------------------------------------------- /test/do-fetch.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import fetchMock from 'fetch-mock'; 3 | import { mount } from 'enzyme'; 4 | import { jsonResponse } from './responses'; 5 | import { Fetch, clearRequestCache, clearResponseCache } from '../src'; 6 | 7 | // Some time for the mock fetches to resolve 8 | const networkTimeout = 10; 9 | 10 | beforeEach(() => { 11 | clearRequestCache(); 12 | clearResponseCache(); 13 | }); 14 | 15 | describe('doFetch()', () => { 16 | test('`doFetch()` returns a promise that resolves with the same object as `afterFetch`', done => { 17 | fetchMock.get( 18 | '/test/succeeds/dofetch-promise', 19 | new Promise(resolve => { 20 | resolve(jsonResponse()); 21 | }) 22 | ); 23 | 24 | expect.assertions(2); 25 | const afterFetchMock = jest.fn(); 26 | const childrenMock = jest.fn(); 27 | 28 | mount( 29 | 35 | ); 36 | 37 | const { doFetch } = childrenMock.mock.calls[0][0]; 38 | doFetch().then(afterFetchInfo => { 39 | setTimeout(() => { 40 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 41 | expect(afterFetchMock).toBeCalledWith(afterFetchInfo); 42 | done(); 43 | }); 44 | }); 45 | }); 46 | 47 | test('`doFetch()` returns a promise that resolves _even_ when there was an error', done => { 48 | fetchMock.get( 49 | '/test/fails/dofetch-promise', 50 | new Promise((resolve, reject) => { 51 | reject({ 52 | message: 'Network error', 53 | }); 54 | }) 55 | ); 56 | 57 | expect.assertions(1); 58 | const childrenMock = jest.fn(); 59 | 60 | mount(); 61 | 62 | const { doFetch } = childrenMock.mock.calls[0][0]; 63 | doFetch().then(afterFetchInfo => { 64 | expect(afterFetchInfo).toMatchObject({ 65 | url: '/test/fails/dofetch-promise', 66 | error: { 67 | message: 'Network error', 68 | }, 69 | failed: true, 70 | didUnmount: false, 71 | data: null, 72 | }); 73 | done(); 74 | }); 75 | }); 76 | }); 77 | 78 | describe('same-component doFetch() with caching (gh-151)', () => { 79 | test('doFetch() with URL and another HTTP method', done => { 80 | // expect.assertions(8); 81 | const onResponseMock = jest.fn(); 82 | const beforeFetchMock = jest.fn(); 83 | const afterFetchMock = jest.fn(); 84 | 85 | let run = 1; 86 | let renderCount = 0; 87 | 88 | mount( 89 | 95 | {options => { 96 | renderCount++; 97 | 98 | // Wait for things to be placed in the cache. 99 | // This occurs on the third render: 100 | // 1st. component mounts 101 | // 2nd. fetch begins 102 | // 3rd. fetch ends 103 | if (run === 1 && renderCount === 3) { 104 | expect(options).toEqual( 105 | expect.objectContaining({ 106 | fetching: false, 107 | data: { 108 | books: [1, 42, 150], 109 | }, 110 | error: null, 111 | failed: false, 112 | requestName: 'anonymousRequest', 113 | url: '/test/succeeds/json-one', 114 | }) 115 | ); 116 | 117 | // We need a timeout here to prevent a race condition 118 | // with the assertions after the component mounts. 119 | setTimeout(() => { 120 | run++; 121 | renderCount = 0; 122 | options.doFetch({ 123 | method: 'patch', 124 | url: '/test/succeeds/patch', 125 | }); 126 | 127 | // Now we need another timeout to allow for the fetch 128 | // to occur. 129 | setTimeout(() => { 130 | done(); 131 | }, networkTimeout); 132 | }, networkTimeout * 2); 133 | } 134 | 135 | if (run === 2) { 136 | if (renderCount === 1) { 137 | expect(options).toEqual( 138 | expect.objectContaining({ 139 | fetching: true, 140 | data: { 141 | books: [1, 42, 150], 142 | }, 143 | error: null, 144 | failed: false, 145 | requestName: 'anonymousRequest', 146 | url: '/test/succeeds/patch', 147 | }) 148 | ); 149 | } 150 | if (renderCount === 2) { 151 | expect(fetchMock.calls('/test/succeeds/patch').length).toBe(1); 152 | expect(options).toEqual( 153 | expect.objectContaining({ 154 | fetching: false, 155 | data: { 156 | movies: [1], 157 | }, 158 | error: null, 159 | failed: false, 160 | requestName: 'anonymousRequest', 161 | url: '/test/succeeds/patch', 162 | }) 163 | ); 164 | } 165 | if (renderCount === 3) { 166 | done.fail(); 167 | } 168 | } 169 | }} 170 | 171 | ); 172 | 173 | setTimeout(() => { 174 | // NOTE: this is for adding stuff to the cache. 175 | // This DOES NOT test the cache-only behavior! 176 | expect(fetchMock.calls('/test/succeeds/json-one').length).toBe(1); 177 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 178 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 179 | expect(afterFetchMock).toBeCalledWith( 180 | expect.objectContaining({ 181 | url: '/test/succeeds/json-one', 182 | error: null, 183 | failed: false, 184 | didUnmount: false, 185 | data: { 186 | books: [1, 42, 150], 187 | }, 188 | }) 189 | ); 190 | expect(onResponseMock).toHaveBeenCalledTimes(1); 191 | expect(onResponseMock).toBeCalledWith( 192 | null, 193 | expect.objectContaining({ 194 | ok: true, 195 | status: 200, 196 | statusText: 'OK', 197 | data: { 198 | books: [1, 42, 150], 199 | }, 200 | }) 201 | ); 202 | }, networkTimeout); 203 | }); 204 | 205 | test('doFetch() with request key and URL', done => { 206 | // expect.assertions(8); 207 | const onResponseMock = jest.fn(); 208 | const beforeFetchMock = jest.fn(); 209 | const afterFetchMock = jest.fn(); 210 | 211 | let run = 1; 212 | let renderCount = 0; 213 | 214 | mount( 215 | 221 | {options => { 222 | renderCount++; 223 | 224 | // Wait for things to be placed in the cache. 225 | // This occurs on the third render: 226 | // 1st. component mounts 227 | // 2nd. fetch begins 228 | // 3rd. fetch ends 229 | if (run === 1 && renderCount === 3) { 230 | expect(options).toEqual( 231 | expect.objectContaining({ 232 | fetching: false, 233 | data: { 234 | books: [1, 42, 150], 235 | }, 236 | error: null, 237 | failed: false, 238 | requestName: 'anonymousRequest', 239 | url: '/test/succeeds/json-one', 240 | }) 241 | ); 242 | 243 | // We need a timeout here to prevent a race condition 244 | // with the assertions after the component mounts. 245 | setTimeout(() => { 246 | run++; 247 | renderCount = 0; 248 | 249 | options.doFetch({ 250 | requestKey: 'sandwiches', 251 | url: '/test/succeeds/json-two', 252 | }); 253 | 254 | // Now we need another timeout to allow for the fetch 255 | // to occur. 256 | setTimeout(() => { 257 | done(); 258 | }, networkTimeout); 259 | }, networkTimeout * 2); 260 | } 261 | 262 | if (run === 2) { 263 | if (renderCount === 1) { 264 | expect(options).toEqual( 265 | expect.objectContaining({ 266 | fetching: true, 267 | data: { 268 | books: [1, 42, 150], 269 | }, 270 | error: null, 271 | failed: false, 272 | requestKey: 'sandwiches', 273 | requestName: 'anonymousRequest', 274 | url: '/test/succeeds/json-two', 275 | }) 276 | ); 277 | } 278 | if (renderCount === 2) { 279 | expect(options).toEqual( 280 | expect.objectContaining({ 281 | fetching: false, 282 | data: { 283 | authors: [22, 13], 284 | }, 285 | error: null, 286 | failed: false, 287 | requestKey: 'sandwiches', 288 | requestName: 'anonymousRequest', 289 | url: '/test/succeeds/json-two', 290 | }) 291 | ); 292 | } 293 | if (renderCount === 3) { 294 | done.fail(); 295 | } 296 | } 297 | }} 298 | 299 | ); 300 | 301 | setTimeout(() => { 302 | // NOTE: this is for adding stuff to the cache. 303 | // This DOES NOT test the cache-only behavior! 304 | expect(fetchMock.calls('/test/succeeds/json-one').length).toBe(1); 305 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 306 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 307 | expect(afterFetchMock).toBeCalledWith( 308 | expect.objectContaining({ 309 | url: '/test/succeeds/json-one', 310 | error: null, 311 | failed: false, 312 | didUnmount: false, 313 | data: { 314 | books: [1, 42, 150], 315 | }, 316 | }) 317 | ); 318 | expect(onResponseMock).toHaveBeenCalledTimes(1); 319 | expect(onResponseMock).toBeCalledWith( 320 | null, 321 | expect.objectContaining({ 322 | ok: true, 323 | status: 200, 324 | statusText: 'OK', 325 | data: { 326 | books: [1, 42, 150], 327 | }, 328 | }) 329 | ); 330 | }, networkTimeout); 331 | }); 332 | 333 | // Note: this does not test dedupe due to the fact that the requests 334 | // resolve too quickly. 335 | test('doFetch(); testing cancelation', done => { 336 | // expect.assertions(8); 337 | const onResponseMock = jest.fn(); 338 | const beforeFetchMock = jest.fn(); 339 | const afterFetchMock = jest.fn(); 340 | 341 | let run = 1; 342 | let renderCount = 0; 343 | 344 | mount( 345 | 351 | {options => { 352 | renderCount++; 353 | 354 | // Wait for things to be placed in the cache. 355 | // This occurs on the third render: 356 | // 1st. component mounts 357 | // 2nd. fetch begins 358 | // 3rd. fetch ends 359 | if (run === 1 && renderCount === 3) { 360 | expect(options).toEqual( 361 | expect.objectContaining({ 362 | fetching: false, 363 | data: { 364 | books: [1, 42, 150], 365 | }, 366 | error: null, 367 | failed: false, 368 | requestName: 'anonymousRequest', 369 | url: '/test/succeeds/json-one', 370 | }) 371 | ); 372 | 373 | // We need a timeout here to prevent a race condition 374 | // with the assertions after the component mounts. 375 | setTimeout(() => { 376 | run++; 377 | renderCount = 0; 378 | 379 | options.doFetch({ 380 | requestKey: 'sandwiches', 381 | url: '/test/succeeds/json-two', 382 | }); 383 | 384 | options.doFetch({ 385 | requestKey: 'sandwiches', 386 | url: '/test/succeeds/json-two', 387 | }); 388 | 389 | // Now we need another timeout to allow for the fetch 390 | // to occur. 391 | setTimeout(() => { 392 | done(); 393 | }, networkTimeout); 394 | }, networkTimeout * 2); 395 | } 396 | 397 | if (run === 2) { 398 | if (renderCount === 1) { 399 | expect(options).toEqual( 400 | expect.objectContaining({ 401 | fetching: true, 402 | data: { 403 | books: [1, 42, 150], 404 | }, 405 | error: null, 406 | failed: false, 407 | requestKey: 'sandwiches', 408 | requestName: 'anonymousRequest', 409 | url: '/test/succeeds/json-two', 410 | }) 411 | ); 412 | } 413 | if (renderCount === 2) { 414 | expect(options).toEqual( 415 | expect.objectContaining({ 416 | fetching: false, 417 | data: { 418 | books: [1, 42, 150], 419 | }, 420 | failed: true, 421 | requestKey: 'sandwiches', 422 | requestName: 'anonymousRequest', 423 | url: '/test/succeeds/json-two', 424 | }) 425 | ); 426 | } 427 | 428 | // This is the 2nd doFetch(). It is difficult to update 429 | // the `run` for that fetch, so we just use the renderCounts. 430 | else if (renderCount === 3) { 431 | expect(options).toEqual( 432 | expect.objectContaining({ 433 | fetching: true, 434 | data: { 435 | books: [1, 42, 150], 436 | }, 437 | error: null, 438 | failed: false, 439 | requestKey: 'sandwiches', 440 | requestName: 'anonymousRequest', 441 | url: '/test/succeeds/json-two', 442 | }) 443 | ); 444 | } else if (renderCount === 4) { 445 | expect(options).toEqual( 446 | expect.objectContaining({ 447 | fetching: false, 448 | data: { 449 | authors: [22, 13], 450 | }, 451 | error: null, 452 | failed: false, 453 | requestKey: 'sandwiches', 454 | requestName: 'anonymousRequest', 455 | url: '/test/succeeds/json-two', 456 | }) 457 | ); 458 | } 459 | if (renderCount > 4) { 460 | done.fail(); 461 | } 462 | } 463 | }} 464 | 465 | ); 466 | 467 | setTimeout(() => { 468 | // NOTE: this is for adding stuff to the cache. 469 | // This DOES NOT test the cache-only behavior! 470 | expect(fetchMock.calls('/test/succeeds/json-one').length).toBe(1); 471 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 472 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 473 | expect(afterFetchMock).toBeCalledWith( 474 | expect.objectContaining({ 475 | url: '/test/succeeds/json-one', 476 | error: null, 477 | failed: false, 478 | didUnmount: false, 479 | data: { 480 | books: [1, 42, 150], 481 | }, 482 | }) 483 | ); 484 | expect(onResponseMock).toHaveBeenCalledTimes(1); 485 | expect(onResponseMock).toBeCalledWith( 486 | null, 487 | expect.objectContaining({ 488 | ok: true, 489 | status: 200, 490 | statusText: 'OK', 491 | data: { 492 | books: [1, 42, 150], 493 | }, 494 | }) 495 | ); 496 | }, networkTimeout); 497 | }); 498 | }); 499 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import fetchMock from 'fetch-mock'; 3 | import { shallow, mount } from 'enzyme'; 4 | import { 5 | Fetch, 6 | clearRequestCache, 7 | getRequestKey, 8 | clearResponseCache, 9 | } from '../src'; 10 | import { successfulResponse, jsonResponse } from './responses'; 11 | 12 | let success = true; 13 | // This could be improved by adding the URL to the JSON response 14 | fetchMock.get('/test/variable', () => { 15 | if (success) { 16 | return new Promise(resolve => { 17 | resolve(jsonResponse()); 18 | }); 19 | } else { 20 | return new Promise((resolve, reject) => { 21 | reject({ 22 | message: 'Network error', 23 | }); 24 | }); 25 | } 26 | }); 27 | 28 | // Some time for the mock fetches to resolve 29 | const networkTimeout = 10; 30 | 31 | beforeEach(() => { 32 | success = true; 33 | clearRequestCache(); 34 | clearResponseCache(); 35 | }); 36 | 37 | describe('rendering', () => { 38 | test('renders null with no render prop', () => { 39 | const wrapper = shallow(); 40 | expect(wrapper.type()).toBe(null); 41 | expect(fetchMock.calls('/test/hangs').length).toBe(1); 42 | }); 43 | 44 | test('renders what you return from the render prop', () => { 45 | const wrapper = shallow( 46 | } lazy={true} /> 47 | ); 48 | expect(wrapper.type()).toBe('a'); 49 | expect(fetchMock.calls('/test/hangs').length).toBe(0); 50 | }); 51 | 52 | test('passes the right object shape to the render function', () => { 53 | const mockRender = jest.fn().mockReturnValue(null); 54 | shallow( 55 | 61 | ); 62 | const requestKey = getRequestKey({ 63 | url: '/test/hangs', 64 | method: 'GET', 65 | }); 66 | expect(mockRender).toHaveBeenCalledTimes(1); 67 | expect(mockRender).toBeCalledWith( 68 | expect.objectContaining({ 69 | requestName: 'tester', 70 | requestKey, 71 | url: '/test/hangs', 72 | fetching: false, 73 | failed: false, 74 | response: null, 75 | data: null, 76 | error: null, 77 | doFetch: expect.any(Function), 78 | }) 79 | ); 80 | expect(fetchMock.calls('/test/hangs').length).toBe(0); 81 | }); 82 | }); 83 | 84 | describe('init props', () => { 85 | it('should call fetch with the correct init', () => { 86 | const signal = new AbortSignal(); 87 | mount( 88 | 105 | ); 106 | 107 | expect(fetchMock.calls('/test/hangs').length).toBe(1); 108 | 109 | expect(fetchMock.lastCall('/test/hangs')).toEqual([ 110 | '/test/hangs', 111 | { 112 | method: 'HEAD', 113 | body: 'cheese', 114 | headers: { 115 | csrf: 'wat', 116 | }, 117 | credentials: 'include', 118 | mode: 'websocket', 119 | cache: 'reload', 120 | redirect: 'error', 121 | referrer: 'spaghetti', 122 | referrerPolicy: 'unsafe-url', 123 | integrity: 'sha-over-9000', 124 | keepalive: false, 125 | signal: signal, 126 | }, 127 | ]); 128 | }); 129 | }); 130 | 131 | describe('successful requests', () => { 132 | test('it calls beforeFetch/afterFetch with the right arguments', done => { 133 | fetchMock.get( 134 | '/test/succeeds/first', 135 | new Promise(resolve => { 136 | resolve(jsonResponse()); 137 | }) 138 | ); 139 | 140 | expect.assertions(4); 141 | const beforeFetchMock = jest.fn(); 142 | const afterFetchMock = jest.fn(); 143 | 144 | const requestKey = getRequestKey({ 145 | url: '/test/succeeds/first', 146 | method: 'GET', 147 | }); 148 | 149 | mount( 150 | 155 | ); 156 | 157 | setTimeout(() => { 158 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 159 | expect(beforeFetchMock).toBeCalledWith( 160 | expect.objectContaining({ 161 | url: '/test/succeeds/first', 162 | requestKey, 163 | }) 164 | ); 165 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 166 | expect(afterFetchMock).toBeCalledWith( 167 | expect.objectContaining({ 168 | url: '/test/succeeds/first', 169 | error: null, 170 | failed: false, 171 | didUnmount: false, 172 | data: { 173 | books: [1, 42, 150], 174 | }, 175 | }) 176 | ); 177 | done(); 178 | }, networkTimeout); 179 | }); 180 | 181 | test('it accepts a custom `responseType`, and calls afterFetch with the right arguments', done => { 182 | fetchMock.get( 183 | '/test/succeeds/second', 184 | new Promise(resolve => { 185 | resolve(successfulResponse()); 186 | }) 187 | ); 188 | 189 | expect.assertions(2); 190 | const afterFetchMock = jest.fn(); 191 | 192 | mount( 193 | 198 | ); 199 | 200 | setTimeout(() => { 201 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 202 | expect(afterFetchMock).toBeCalledWith( 203 | expect.objectContaining({ 204 | url: '/test/succeeds/second', 205 | error: null, 206 | failed: false, 207 | didUnmount: false, 208 | data: 'hi', 209 | }) 210 | ); 211 | done(); 212 | }, networkTimeout); 213 | }); 214 | 215 | test('it sets data to `null` if the contentType does not parse the data', done => { 216 | fetchMock.get( 217 | '/test/succeeds/second-mismatch-content-type', 218 | new Promise(resolve => { 219 | resolve(successfulResponse()); 220 | }) 221 | ); 222 | 223 | expect.assertions(2); 224 | const afterFetchMock = jest.fn(); 225 | 226 | mount( 227 | 232 | ); 233 | 234 | setTimeout(() => { 235 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 236 | expect(afterFetchMock).toBeCalledWith( 237 | expect.objectContaining({ 238 | url: '/test/succeeds/second-mismatch-content-type', 239 | error: null, 240 | failed: false, 241 | didUnmount: false, 242 | data: null, 243 | }) 244 | ); 245 | done(); 246 | }, networkTimeout); 247 | }); 248 | 249 | test('it accepts a custom `responseType` as a function, and calls afterFetch with the right arguments', done => { 250 | fetchMock.get( 251 | '/test/succeeds/secondpls', 252 | new Promise(resolve => { 253 | resolve(successfulResponse()); 254 | }) 255 | ); 256 | 257 | expect.assertions(2); 258 | const afterFetchMock = jest.fn(); 259 | 260 | mount( 261 | 'text'} 265 | /> 266 | ); 267 | 268 | setTimeout(() => { 269 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 270 | expect(afterFetchMock).toBeCalledWith( 271 | expect.objectContaining({ 272 | url: '/test/succeeds/secondpls', 273 | error: null, 274 | failed: false, 275 | didUnmount: false, 276 | data: 'hi', 277 | }) 278 | ); 279 | done(); 280 | }, networkTimeout); 281 | }); 282 | 283 | test('`transformData` is used to transform the response data', done => { 284 | fetchMock.get( 285 | '/test/succeeds/third', 286 | new Promise(resolve => { 287 | resolve(jsonResponse()); 288 | }) 289 | ); 290 | 291 | expect.assertions(2); 292 | const afterFetchMock = jest.fn(); 293 | function transformData(data) { 294 | return { 295 | sandwiches: data.books, 296 | }; 297 | } 298 | 299 | mount( 300 | 305 | ); 306 | 307 | setTimeout(() => { 308 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 309 | expect(afterFetchMock).toBeCalledWith( 310 | expect.objectContaining({ 311 | url: '/test/succeeds/third', 312 | error: null, 313 | failed: false, 314 | didUnmount: false, 315 | data: { 316 | sandwiches: [1, 42, 150], 317 | }, 318 | }) 319 | ); 320 | done(); 321 | }, networkTimeout); 322 | }); 323 | }); 324 | 325 | describe('cache strategies', () => { 326 | describe('cache-only', () => { 327 | test('errors when there is nothing in the cache', done => { 328 | expect.assertions(5); 329 | const onResponseMock = jest.fn(); 330 | const beforeFetchMock = jest.fn(); 331 | const afterFetchMock = jest.fn(); 332 | 333 | mount( 334 | 342 | ); 343 | 344 | Promise.resolve() 345 | .then(() => { 346 | expect( 347 | fetchMock.calls('/test/succeeds/cache-only-empty').length 348 | ).toBe(0); 349 | expect(beforeFetchMock).toHaveBeenCalledTimes(0); 350 | expect(afterFetchMock).toHaveBeenCalledTimes(0); 351 | expect(onResponseMock).toHaveBeenCalledTimes(1); 352 | 353 | expect(onResponseMock).toBeCalledWith( 354 | expect.objectContaining({ 355 | message: 'Response for "meepmeep" not found in cache.', 356 | }), 357 | null 358 | ); 359 | 360 | done(); 361 | }) 362 | .catch(done.fail); 363 | }); 364 | 365 | test('respects `cacheResponse: false`, erroring', done => { 366 | expect.assertions(11); 367 | const onResponseMock = jest.fn(); 368 | const beforeFetchMock = jest.fn(); 369 | const afterFetchMock = jest.fn(); 370 | 371 | // First, we need to add some stuff to the cache 372 | mount( 373 | 380 | ); 381 | 382 | setTimeout(() => { 383 | // NOTE: this is for adding stuff to the cache. 384 | // This DOES NOT test the cache-only behavior! 385 | expect(fetchMock.calls('/test/succeeds/cache-only-full').length).toBe( 386 | 1 387 | ); 388 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 389 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 390 | expect(afterFetchMock).toBeCalledWith( 391 | expect.objectContaining({ 392 | url: '/test/succeeds/cache-only-full', 393 | error: null, 394 | didUnmount: false, 395 | data: { 396 | books: [1, 42, 150], 397 | }, 398 | }) 399 | ); 400 | expect(onResponseMock).toHaveBeenCalledTimes(1); 401 | expect(onResponseMock).toBeCalledWith( 402 | null, 403 | expect.objectContaining({ 404 | ok: true, 405 | status: 200, 406 | statusText: 'OK', 407 | data: { 408 | books: [1, 42, 150], 409 | }, 410 | }) 411 | ); 412 | 413 | const beforeFetchMock2 = jest.fn(); 414 | const afterFetchMock2 = jest.fn(); 415 | const onResponseMock2 = jest.fn(); 416 | 417 | mount( 418 | 426 | ); 427 | 428 | setTimeout(() => { 429 | expect(fetchMock.calls('/test/succeeds/cache-only-full').length).toBe( 430 | 1 431 | ); 432 | expect(beforeFetchMock2).toHaveBeenCalledTimes(0); 433 | expect(afterFetchMock2).toHaveBeenCalledTimes(0); 434 | expect(onResponseMock2).toHaveBeenCalledTimes(1); 435 | expect(onResponseMock2).toBeCalledWith( 436 | expect.objectContaining({ 437 | message: 'Response for "meepmeep" not found in cache.', 438 | }), 439 | null 440 | ); 441 | done(); 442 | }, networkTimeout); 443 | }, networkTimeout); 444 | }); 445 | 446 | test('it returns the cached data when found', done => { 447 | expect.assertions(11); 448 | const onResponseMock = jest.fn(); 449 | const beforeFetchMock = jest.fn(); 450 | const afterFetchMock = jest.fn(); 451 | 452 | // First, we need to add some stuff to the cache 453 | mount( 454 | 460 | ); 461 | 462 | setTimeout(() => { 463 | // NOTE: this is for adding stuff to the cache. 464 | // This DOES NOT test the cache-only behavior! 465 | expect(fetchMock.calls('/test/succeeds/cache-only-full').length).toBe( 466 | 1 467 | ); 468 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 469 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 470 | expect(afterFetchMock).toBeCalledWith( 471 | expect.objectContaining({ 472 | url: '/test/succeeds/cache-only-full', 473 | error: null, 474 | failed: false, 475 | didUnmount: false, 476 | data: { 477 | books: [1, 42, 150], 478 | }, 479 | }) 480 | ); 481 | expect(onResponseMock).toHaveBeenCalledTimes(1); 482 | expect(onResponseMock).toBeCalledWith( 483 | null, 484 | expect.objectContaining({ 485 | ok: true, 486 | status: 200, 487 | statusText: 'OK', 488 | data: { 489 | books: [1, 42, 150], 490 | }, 491 | }) 492 | ); 493 | 494 | const beforeFetchMock2 = jest.fn(); 495 | const afterFetchMock2 = jest.fn(); 496 | const onResponseMock2 = jest.fn(); 497 | 498 | mount( 499 | 506 | ); 507 | 508 | setTimeout(() => { 509 | expect(fetchMock.calls('/test/succeeds/cache-only-full').length).toBe( 510 | 1 511 | ); 512 | expect(beforeFetchMock2).toHaveBeenCalledTimes(0); 513 | expect(afterFetchMock2).toHaveBeenCalledTimes(0); 514 | expect(onResponseMock2).toHaveBeenCalledTimes(1); 515 | expect(onResponseMock2).toBeCalledWith( 516 | null, 517 | expect.objectContaining({ 518 | ok: true, 519 | status: 200, 520 | statusText: 'OK', 521 | data: { 522 | books: [1, 42, 150], 523 | }, 524 | }) 525 | ); 526 | done(); 527 | }, networkTimeout); 528 | }, networkTimeout); 529 | }); 530 | 531 | test('it returns the cached data when found; POST method', done => { 532 | expect.assertions(11); 533 | const onResponseMock = jest.fn(); 534 | const beforeFetchMock = jest.fn(); 535 | const afterFetchMock = jest.fn(); 536 | 537 | // First, we need to add some stuff to the cache 538 | mount( 539 | 548 | ); 549 | 550 | setTimeout(() => { 551 | // NOTE: this is for adding stuff to the cache. 552 | // This DOES NOT test the cache-only behavior! 553 | expect(fetchMock.calls('/test/succeeds/cache-only-full').length).toBe( 554 | 1 555 | ); 556 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 557 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 558 | expect(afterFetchMock).toBeCalledWith( 559 | expect.objectContaining({ 560 | url: '/test/succeeds/cache-only-full', 561 | error: null, 562 | failed: false, 563 | didUnmount: false, 564 | data: { 565 | books: [1, 42, 150], 566 | }, 567 | }) 568 | ); 569 | expect(onResponseMock).toHaveBeenCalledTimes(1); 570 | expect(onResponseMock).toBeCalledWith( 571 | null, 572 | expect.objectContaining({ 573 | ok: true, 574 | status: 200, 575 | statusText: 'OK', 576 | data: { 577 | books: [1, 42, 150], 578 | }, 579 | }) 580 | ); 581 | 582 | const beforeFetchMock2 = jest.fn(); 583 | const afterFetchMock2 = jest.fn(); 584 | const onResponseMock2 = jest.fn(); 585 | 586 | mount( 587 | 597 | ); 598 | 599 | setTimeout(() => { 600 | expect(fetchMock.calls('/test/succeeds/cache-only-full').length).toBe( 601 | 1 602 | ); 603 | expect(beforeFetchMock2).toHaveBeenCalledTimes(0); 604 | expect(afterFetchMock2).toHaveBeenCalledTimes(0); 605 | expect(onResponseMock2).toHaveBeenCalledTimes(1); 606 | expect(onResponseMock2).toBeCalledWith( 607 | null, 608 | expect.objectContaining({ 609 | ok: true, 610 | status: 200, 611 | statusText: 'OK', 612 | data: { 613 | books: [1, 42, 150], 614 | }, 615 | }) 616 | ); 617 | done(); 618 | }, networkTimeout); 619 | }, networkTimeout); 620 | }); 621 | }); 622 | 623 | describe('cache-first', () => { 624 | // By "identical" I mean that their request keys are the same 625 | test('it only makes one network request when two "identical" components are mounted', done => { 626 | fetchMock.get( 627 | '/test/succeeds/cache-first', 628 | new Promise(resolve => { 629 | resolve(jsonResponse()); 630 | }) 631 | ); 632 | 633 | expect.assertions(11); 634 | const onResponseMock = jest.fn(); 635 | const beforeFetchMock = jest.fn(); 636 | const afterFetchMock = jest.fn(); 637 | 638 | mount( 639 | 645 | ); 646 | 647 | setTimeout(() => { 648 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 649 | expect(beforeFetchMock).toBeCalledWith( 650 | expect.objectContaining({ 651 | url: '/test/succeeds/cache-first', 652 | }) 653 | ); 654 | 655 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 656 | expect(afterFetchMock).toBeCalledWith( 657 | expect.objectContaining({ 658 | url: '/test/succeeds/cache-first', 659 | error: null, 660 | failed: false, 661 | didUnmount: false, 662 | data: { 663 | books: [1, 42, 150], 664 | }, 665 | }) 666 | ); 667 | expect(onResponseMock).toHaveBeenCalledTimes(1); 668 | expect(onResponseMock).toBeCalledWith( 669 | null, 670 | expect.objectContaining({ 671 | ok: true, 672 | status: 200, 673 | statusText: 'OK', 674 | data: { 675 | books: [1, 42, 150], 676 | }, 677 | }) 678 | ); 679 | 680 | const beforeFetchMock2 = jest.fn(); 681 | const afterFetchMock2 = jest.fn(); 682 | const onResponseMock2 = jest.fn(); 683 | 684 | mount( 685 | 691 | ); 692 | 693 | setTimeout(() => { 694 | expect(fetchMock.calls('/test/succeeds/cache-first').length).toBe(1); 695 | expect(beforeFetchMock2).toHaveBeenCalledTimes(0); 696 | expect(afterFetchMock2).toHaveBeenCalledTimes(0); 697 | expect(onResponseMock2).toHaveBeenCalledTimes(1); 698 | expect(onResponseMock2).toBeCalledWith( 699 | null, 700 | expect.objectContaining({ 701 | ok: true, 702 | status: 200, 703 | statusText: 'OK', 704 | data: { 705 | books: [1, 42, 150], 706 | }, 707 | }) 708 | ); 709 | done(); 710 | }, 10); 711 | }, networkTimeout); 712 | }); 713 | }); 714 | 715 | describe('network-only', () => { 716 | // By "identical" I mean that their request keys are the same 717 | test('it makes two network requests, even when two "identical" components are mounted', done => { 718 | fetchMock.get('/test/succeeds/network-only', () => { 719 | return new Promise(resolve => { 720 | resolve(jsonResponse()); 721 | }); 722 | }); 723 | 724 | expect.assertions(13); 725 | const onResponseMock = jest.fn(); 726 | const beforeFetchMock = jest.fn(); 727 | const afterFetchMock = jest.fn(); 728 | 729 | mount( 730 | 737 | ); 738 | 739 | setTimeout(() => { 740 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 741 | expect(beforeFetchMock).toBeCalledWith( 742 | expect.objectContaining({ 743 | url: '/test/succeeds/network-only', 744 | }) 745 | ); 746 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 747 | expect(afterFetchMock).toBeCalledWith( 748 | expect.objectContaining({ 749 | url: '/test/succeeds/network-only', 750 | error: null, 751 | failed: false, 752 | didUnmount: false, 753 | data: { 754 | books: [1, 42, 150], 755 | }, 756 | }) 757 | ); 758 | expect(onResponseMock).toHaveBeenCalledTimes(1); 759 | expect(onResponseMock).toBeCalledWith( 760 | null, 761 | expect.objectContaining({ 762 | ok: true, 763 | status: 200, 764 | statusText: 'OK', 765 | data: { 766 | books: [1, 42, 150], 767 | }, 768 | }) 769 | ); 770 | 771 | const onResponseMock2 = jest.fn(); 772 | const beforeFetchMock2 = jest.fn(); 773 | const afterFetchMock2 = jest.fn(); 774 | 775 | mount( 776 | 783 | ); 784 | 785 | setTimeout(() => { 786 | expect(fetchMock.calls('/test/succeeds/network-only').length).toBe(2); 787 | expect(beforeFetchMock2).toHaveBeenCalledTimes(1); 788 | expect(beforeFetchMock2).toBeCalledWith( 789 | expect.objectContaining({ 790 | url: '/test/succeeds/network-only', 791 | }) 792 | ); 793 | expect(afterFetchMock2).toHaveBeenCalledTimes(1); 794 | expect(afterFetchMock2).toBeCalledWith( 795 | expect.objectContaining({ 796 | url: '/test/succeeds/network-only', 797 | error: null, 798 | failed: false, 799 | didUnmount: false, 800 | data: { 801 | books: [1, 42, 150], 802 | }, 803 | }) 804 | ); 805 | expect(onResponseMock2).toHaveBeenCalledTimes(1); 806 | expect(onResponseMock2).toBeCalledWith( 807 | null, 808 | expect.objectContaining({ 809 | ok: true, 810 | status: 200, 811 | statusText: 'OK', 812 | data: { 813 | books: [1, 42, 150], 814 | }, 815 | }) 816 | ); 817 | done(); 818 | }, networkTimeout); 819 | }, networkTimeout); 820 | }); 821 | }); 822 | 823 | describe('cache-and-network', () => { 824 | // By "identical" I mean that their request keys are the same 825 | test('it makes two network requests, even when two "identical" components are mounted', done => { 826 | expect.assertions(12); 827 | const onResponseMock = jest.fn(); 828 | const beforeFetchMock = jest.fn(); 829 | const afterFetchMock = jest.fn(); 830 | 831 | mount( 832 | 839 | ); 840 | 841 | setTimeout(() => { 842 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 843 | expect(beforeFetchMock).toBeCalledWith( 844 | expect.objectContaining({ 845 | url: '/test/succeeds', 846 | }) 847 | ); 848 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 849 | expect(afterFetchMock).toBeCalledWith( 850 | expect.objectContaining({ 851 | url: '/test/succeeds', 852 | error: null, 853 | failed: false, 854 | didUnmount: false, 855 | data: { 856 | books: [1, 42, 150], 857 | }, 858 | }) 859 | ); 860 | expect(onResponseMock).toHaveBeenCalledTimes(1); 861 | expect(onResponseMock).toBeCalledWith( 862 | null, 863 | expect.objectContaining({ 864 | ok: true, 865 | status: 200, 866 | statusText: 'OK', 867 | data: { 868 | books: [1, 42, 150], 869 | }, 870 | }) 871 | ); 872 | 873 | const onResponseMock2 = jest.fn(); 874 | const beforeFetchMock2 = jest.fn(); 875 | const afterFetchMock2 = jest.fn(); 876 | 877 | mount( 878 | 885 | ); 886 | 887 | setTimeout(() => { 888 | expect(fetchMock.calls('/test/succeeds').length).toBe(2); 889 | expect(beforeFetchMock2).toHaveBeenCalledTimes(1); 890 | expect(beforeFetchMock2).toBeCalledWith( 891 | expect.objectContaining({ 892 | url: '/test/succeeds', 893 | }) 894 | ); 895 | expect(afterFetchMock2).toHaveBeenCalledTimes(1); 896 | expect(afterFetchMock).toBeCalledWith( 897 | expect.objectContaining({ 898 | url: '/test/succeeds', 899 | error: null, 900 | failed: false, 901 | didUnmount: false, 902 | data: { 903 | books: [1, 42, 150], 904 | }, 905 | }) 906 | ); 907 | // Two calls: the first is for the cache, and the second is 908 | // for when the network returns success 909 | expect(onResponseMock2).toHaveBeenCalledTimes(2); 910 | done(); 911 | }, networkTimeout); 912 | }, networkTimeout); 913 | }); 914 | 915 | test('handles failure correctly', done => { 916 | expect.assertions(13); 917 | const onResponseMock = jest.fn(); 918 | const beforeFetchMock = jest.fn(); 919 | const afterFetchMock = jest.fn(); 920 | 921 | mount( 922 | 929 | ); 930 | 931 | setTimeout(() => { 932 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 933 | expect(beforeFetchMock).toBeCalledWith( 934 | expect.objectContaining({ 935 | url: '/test/variable', 936 | }) 937 | ); 938 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 939 | expect(afterFetchMock).toBeCalledWith( 940 | expect.objectContaining({ 941 | url: '/test/variable', 942 | error: null, 943 | failed: false, 944 | didUnmount: false, 945 | data: { 946 | books: [1, 42, 150], 947 | }, 948 | }) 949 | ); 950 | expect(onResponseMock).toHaveBeenCalledTimes(1); 951 | expect(onResponseMock).toBeCalledWith( 952 | null, 953 | expect.objectContaining({ 954 | ok: true, 955 | status: 200, 956 | statusText: 'OK', 957 | data: { 958 | books: [1, 42, 150], 959 | }, 960 | }) 961 | ); 962 | 963 | success = false; 964 | 965 | const onResponseMock2 = jest.fn(); 966 | const beforeFetchMock2 = jest.fn(); 967 | const afterFetchMock2 = jest.fn(); 968 | 969 | mount( 970 | 977 | ); 978 | 979 | setTimeout(() => { 980 | expect(fetchMock.calls('/test/variable').length).toBe(2); 981 | expect(beforeFetchMock2).toHaveBeenCalledTimes(1); 982 | expect(beforeFetchMock2).toBeCalledWith( 983 | expect.objectContaining({ 984 | url: '/test/variable', 985 | }) 986 | ); 987 | expect(afterFetchMock2).toHaveBeenCalledTimes(1); 988 | expect(afterFetchMock2).toBeCalledWith( 989 | expect.objectContaining({ 990 | url: '/test/variable', 991 | didUnmount: false, 992 | failed: true, 993 | data: { 994 | books: [1, 42, 150], 995 | }, 996 | }) 997 | ); 998 | expect(afterFetchMock2.mock.calls[0][0]).toHaveProperty( 999 | 'error.message', 1000 | 'Network error' 1001 | ); 1002 | // Two calls: the first is for the cache, and the second is 1003 | // for when the network returns success 1004 | expect(onResponseMock2).toHaveBeenCalledTimes(2); 1005 | done(); 1006 | }, networkTimeout); 1007 | }, networkTimeout); 1008 | }); 1009 | }); 1010 | }); 1011 | 1012 | describe('unsuccessful requests', () => { 1013 | test('it calls afterFetch with the right arguments', done => { 1014 | fetchMock.get( 1015 | '/test/fails', 1016 | new Promise((resolve, reject) => { 1017 | reject({ 1018 | message: 'Network error', 1019 | }); 1020 | }) 1021 | ); 1022 | 1023 | expect.assertions(4); 1024 | const afterFetchMock = jest.fn(); 1025 | 1026 | mount(); 1027 | 1028 | setTimeout(() => { 1029 | expect(fetchMock.calls('/test/fails').length).toBe(1); 1030 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 1031 | expect(afterFetchMock).toBeCalledWith( 1032 | expect.objectContaining({ 1033 | url: '/test/fails', 1034 | failed: true, 1035 | didUnmount: false, 1036 | }) 1037 | ); 1038 | expect(afterFetchMock.mock.calls[0][0]).toHaveProperty( 1039 | 'error.message', 1040 | 'Network error' 1041 | ); 1042 | done(); 1043 | }, networkTimeout); 1044 | }); 1045 | }); 1046 | 1047 | describe('request deduplication', () => { 1048 | test('it should dispatch one request with the same key before they resolve', () => { 1049 | const beforeFetchMock1 = jest.fn(); 1050 | const beforeFetchMock2 = jest.fn(); 1051 | 1052 | shallow(); 1053 | shallow(); 1054 | 1055 | expect(beforeFetchMock1).toHaveBeenCalledTimes(1); 1056 | expect(beforeFetchMock2).toHaveBeenCalledTimes(0); 1057 | expect(fetchMock.calls('/test/hangs').length).toBe(1); 1058 | }); 1059 | 1060 | test('it should dispatch two requests, even when the URL is the same, when different keys are specified', () => { 1061 | const beforeFetchMock1 = jest.fn(); 1062 | const beforeFetchMock2 = jest.fn(); 1063 | 1064 | shallow( 1065 | 1066 | ); 1067 | shallow( 1068 | 1069 | ); 1070 | 1071 | expect(beforeFetchMock1).toHaveBeenCalledTimes(1); 1072 | expect(beforeFetchMock2).toHaveBeenCalledTimes(1); 1073 | expect(fetchMock.calls('/test/hangs').length).toBe(2); 1074 | }); 1075 | 1076 | test('it should dispatch one requests, even when the URL is different, when the same key is specified', () => { 1077 | const beforeFetchMock1 = jest.fn(); 1078 | const beforeFetchMock2 = jest.fn(); 1079 | 1080 | shallow( 1081 | 1086 | ); 1087 | shallow( 1088 | 1093 | ); 1094 | 1095 | expect(beforeFetchMock1).toHaveBeenCalledTimes(1); 1096 | expect(beforeFetchMock2).toHaveBeenCalledTimes(0); 1097 | expect(fetchMock.calls('/test/hangs/1').length).toBe(1); 1098 | }); 1099 | 1100 | test('it should dispatch two requests with the same key before they resolve when dedupe:false is passed to both', () => { 1101 | const beforeFetchMock1 = jest.fn(); 1102 | const beforeFetchMock2 = jest.fn(); 1103 | 1104 | shallow( 1105 | 1106 | ); 1107 | shallow( 1108 | 1109 | ); 1110 | 1111 | expect(beforeFetchMock1).toHaveBeenCalledTimes(1); 1112 | expect(beforeFetchMock2).toHaveBeenCalledTimes(1); 1113 | expect(fetchMock.calls('/test/hangs').length).toBe(2); 1114 | }); 1115 | 1116 | test('it should dispatch two requests with the same key before they resolve when dedupe:false is passed to just one of them', () => { 1117 | const beforeFetchMock1 = jest.fn(); 1118 | const beforeFetchMock2 = jest.fn(); 1119 | 1120 | shallow(); 1121 | shallow( 1122 | 1123 | ); 1124 | 1125 | expect(beforeFetchMock1).toHaveBeenCalledTimes(1); 1126 | expect(beforeFetchMock2).toHaveBeenCalledTimes(1); 1127 | expect(fetchMock.calls('/test/hangs').length).toBe(2); 1128 | }); 1129 | 1130 | test('it should dispatch two requests with different keys', () => { 1131 | const beforeFetchMock1 = jest.fn(); 1132 | const beforeFetchMock2 = jest.fn(); 1133 | 1134 | shallow(); 1135 | shallow(); 1136 | 1137 | expect(beforeFetchMock1).toHaveBeenCalledTimes(1); 1138 | expect(beforeFetchMock2).toHaveBeenCalledTimes(1); 1139 | expect(fetchMock.calls('/test/hangs/1').length).toBe(1); 1140 | expect(fetchMock.calls('/test/hangs/2').length).toBe(1); 1141 | }); 1142 | 1143 | test('dedupe:false with successful data should return the proper data', done => { 1144 | fetchMock.get( 1145 | '/test/fails/dedupe-false', 1146 | new Promise((resolve, reject) => { 1147 | reject({ 1148 | message: 'Network error', 1149 | }); 1150 | }) 1151 | ); 1152 | 1153 | const beforeFetchMock = jest.fn(); 1154 | const afterFetchMock = jest.fn(); 1155 | const onResponseMock = jest.fn(); 1156 | shallow( 1157 | 1164 | ); 1165 | 1166 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 1167 | setTimeout(() => { 1168 | expect(fetchMock.calls('/test/fails/dedupe-false').length).toBe(1); 1169 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 1170 | expect(afterFetchMock).toBeCalledWith( 1171 | expect.objectContaining({ 1172 | url: '/test/fails/dedupe-false', 1173 | didUnmount: false, 1174 | failed: true, 1175 | }) 1176 | ); 1177 | expect(afterFetchMock.mock.calls[0][0]).toHaveProperty( 1178 | 'error.message', 1179 | 'Network error' 1180 | ); 1181 | done(); 1182 | }, networkTimeout); 1183 | }); 1184 | }); 1185 | 1186 | // Request cancellation can be tested by checking if `afterFetch` is called 1187 | // with a specific kind of error. 1188 | describe('request cancellation', () => { 1189 | test('it should not cancel when a single request is initiated', () => { 1190 | const afterFetchMock = jest.fn(); 1191 | 1192 | shallow(); 1193 | 1194 | expect(afterFetchMock).toHaveBeenCalledTimes(0); 1195 | }); 1196 | 1197 | test('it should cancel when a double request is initiated via `doFetch`', () => { 1198 | expect.assertions(3); 1199 | jest.useFakeTimers(); 1200 | const afterFetchMock = jest.fn(); 1201 | 1202 | let hasFetched = false; 1203 | shallow( 1204 | { 1208 | if (!hasFetched) { 1209 | hasFetched = true; 1210 | doFetch(); 1211 | } 1212 | }} 1213 | /> 1214 | ); 1215 | 1216 | jest.runOnlyPendingTimers(); 1217 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 1218 | expect(afterFetchMock.mock.calls[0][0]).toHaveProperty( 1219 | 'error.message', 1220 | 'New fetch initiated' 1221 | ); 1222 | expect(afterFetchMock.mock.calls[0][0]).toHaveProperty( 1223 | 'error.name', 1224 | 'AbortError' 1225 | ); 1226 | }); 1227 | 1228 | test('it should cancel when a double request is initiated via prop changes', () => { 1229 | fetchMock.get('/test/hangs/double-request', hangingPromise()); 1230 | 1231 | const afterFetchMock = jest.fn(); 1232 | 1233 | const wrapper = shallow( 1234 | 1235 | ); 1236 | 1237 | wrapper.setProps({ 1238 | url: '/test/hangs/double-request', 1239 | }); 1240 | 1241 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 1242 | expect(afterFetchMock.mock.calls[0][0]).toHaveProperty( 1243 | 'error.message', 1244 | 'New fetch initiated' 1245 | ); 1246 | expect(afterFetchMock.mock.calls[0][0]).toHaveProperty( 1247 | 'error.name', 1248 | 'AbortError' 1249 | ); 1250 | }); 1251 | 1252 | test('it should cancel when the component unmounts', () => { 1253 | const afterFetchMock = jest.fn(); 1254 | 1255 | const wrapper = shallow( 1256 | 1257 | ); 1258 | 1259 | wrapper.unmount(); 1260 | 1261 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 1262 | expect(afterFetchMock.mock.calls[0][0]).toHaveProperty( 1263 | 'error.message', 1264 | 'Component unmounted' 1265 | ); 1266 | expect(afterFetchMock.mock.calls[0][0]).toHaveProperty( 1267 | 'error.name', 1268 | 'AbortError' 1269 | ); 1270 | expect(afterFetchMock).toBeCalledWith( 1271 | expect.objectContaining({ 1272 | url: '/test/hangs', 1273 | didUnmount: true, 1274 | }) 1275 | ); 1276 | }); 1277 | }); 1278 | 1279 | describe('laziness', () => { 1280 | describe('defaults', () => { 1281 | test('is false when just a URL is passed', () => { 1282 | const beforeFetchMock = jest.fn(); 1283 | const afterFetchMock = jest.fn(); 1284 | mount( 1285 | 1290 | ); 1291 | expect(fetchMock.calls('/test/hangs').length).toBe(1); 1292 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 1293 | expect(afterFetchMock).toHaveBeenCalledTimes(0); 1294 | }); 1295 | 1296 | test('is false when method is GET, HEAD, or OPTIONS', () => { 1297 | const beforeFetchMock1 = jest.fn(); 1298 | const beforeFetchMock2 = jest.fn(); 1299 | const afterFetchMock1 = jest.fn(); 1300 | const afterFetchMock2 = jest.fn(); 1301 | 1302 | mount( 1303 | 1309 | ); 1310 | expect(fetchMock.calls('/test/hangs').length).toBe(1); 1311 | expect(beforeFetchMock1).toHaveBeenCalledTimes(1); 1312 | expect(afterFetchMock1).toHaveBeenCalledTimes(0); 1313 | 1314 | mount( 1315 | 1321 | ); 1322 | expect(fetchMock.calls('/test/hangs').length).toBe(2); 1323 | expect(beforeFetchMock1).toHaveBeenCalledTimes(1); 1324 | expect(afterFetchMock2).toHaveBeenCalledTimes(0); 1325 | }); 1326 | 1327 | test('is true when method is POST, PATCH, PUT, or DELETE', () => { 1328 | const beforeFetchMock1 = jest.fn(); 1329 | const beforeFetchMock2 = jest.fn(); 1330 | const beforeFetchMock3 = jest.fn(); 1331 | const beforeFetchMock4 = jest.fn(); 1332 | const afterFetchMock1 = jest.fn(); 1333 | const afterFetchMock2 = jest.fn(); 1334 | const afterFetchMock3 = jest.fn(); 1335 | const afterFetchMock4 = jest.fn(); 1336 | 1337 | mount( 1338 | 1344 | ); 1345 | expect(fetchMock.calls('/test/hangs').length).toBe(0); 1346 | expect(beforeFetchMock1).toHaveBeenCalledTimes(0); 1347 | expect(afterFetchMock1).toHaveBeenCalledTimes(0); 1348 | 1349 | mount( 1350 | 1356 | ); 1357 | expect(fetchMock.calls('/test/hangs').length).toBe(0); 1358 | expect(beforeFetchMock2).toHaveBeenCalledTimes(0); 1359 | expect(afterFetchMock2).toHaveBeenCalledTimes(0); 1360 | 1361 | mount( 1362 | 1368 | ); 1369 | expect(fetchMock.calls('/test/hangs').length).toBe(0); 1370 | expect(beforeFetchMock3).toHaveBeenCalledTimes(0); 1371 | expect(afterFetchMock3).toHaveBeenCalledTimes(0); 1372 | 1373 | mount( 1374 | 1380 | ); 1381 | expect(fetchMock.calls('/test/hangs').length).toBe(0); 1382 | expect(beforeFetchMock4).toHaveBeenCalledTimes(0); 1383 | expect(afterFetchMock4).toHaveBeenCalledTimes(0); 1384 | }); 1385 | }); 1386 | 1387 | describe('manually specifying it', () => { 1388 | test('it should override the default for "read" methods', () => { 1389 | const beforeFetchMock = jest.fn(); 1390 | 1391 | mount( 1392 | 1398 | ); 1399 | 1400 | expect(fetchMock.calls('/test/hangs').length).toBe(0); 1401 | expect(beforeFetchMock).toHaveBeenCalledTimes(0); 1402 | }); 1403 | 1404 | test('it should override the default for "write" methods', () => { 1405 | const beforeFetchMock = jest.fn(); 1406 | 1407 | mount( 1408 | 1414 | ); 1415 | 1416 | expect(fetchMock.calls('/test/hangs').length).toBe(1); 1417 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 1418 | }); 1419 | }); 1420 | 1421 | test('it should be respected when the props change', () => { 1422 | fetchMock.get('/test/hangs/double-request2', hangingPromise()); 1423 | 1424 | const afterFetchMock = jest.fn(); 1425 | 1426 | const wrapper = shallow( 1427 | 1428 | ); 1429 | 1430 | wrapper.setProps({ 1431 | url: '/test/hangs/double-request2', 1432 | }); 1433 | 1434 | expect(fetchMock.calls('/test/hangs').length).toBe(0); 1435 | expect(fetchMock.calls('/test/hangs/double-request2').length).toBe(0); 1436 | }); 1437 | }); 1438 | -------------------------------------------------------------------------------- /test/responses.js: -------------------------------------------------------------------------------- 1 | export function successfulResponse() { 2 | return new Response('hi', { 3 | status: 200, 4 | statusText: 'OK', 5 | }); 6 | } 7 | 8 | export function jsonResponse() { 9 | return new Response('{"books": [1, 42, 150]}', { 10 | status: 200, 11 | statusText: 'OK', 12 | }); 13 | } 14 | 15 | export function jsonResponse2() { 16 | return new Response('{"authors": [22, 13]}', { 17 | status: 200, 18 | statusText: 'OK', 19 | }); 20 | } 21 | 22 | export function jsonResponse3() { 23 | return new Response('{"movies": [1]}', { 24 | status: 200, 25 | statusText: 'OK', 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /test/same-component.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import fetchMock from 'fetch-mock'; 3 | import { mount } from 'enzyme'; 4 | import { Fetch, clearRequestCache, clearResponseCache } from '../src'; 5 | 6 | // Some time for the mock fetches to resolve 7 | const networkTimeout = 10; 8 | 9 | beforeEach(() => { 10 | clearRequestCache(); 11 | clearResponseCache(); 12 | }); 13 | 14 | // Issue 151 describes the 3 situations when requests can be made. This tests 15 | // situation 2. 16 | describe('same-component subsequent requests with caching (gh-151)', () => { 17 | test('it uses a directly-updated request key on subsequent renders', done => { 18 | // expect.assertions(8); 19 | const onResponseMock = jest.fn(); 20 | const beforeFetchMock = jest.fn(); 21 | const afterFetchMock = jest.fn(); 22 | 23 | let run = 1; 24 | let renderCount = 0; 25 | 26 | const wrapper = mount( 27 | 33 | {options => { 34 | if (run === 2) { 35 | // Increment our render count. This allows us to 36 | // test for each of the individual renders involved 37 | // with changing the prop. 38 | renderCount++; 39 | 40 | // This first render is interesting: we basically only have a 41 | // new URL set, but the request has not yet begun. The reason 42 | // for this is because we do the fetch in `componentDidUpdate`. 43 | if (renderCount === 1) { 44 | expect(options).toEqual( 45 | expect.objectContaining({ 46 | requestKey: '1', 47 | fetching: false, 48 | data: { 49 | books: [1, 42, 150], 50 | }, 51 | error: null, 52 | failed: false, 53 | url: '/test/succeeds/json-one', 54 | }) 55 | ); 56 | } else if (renderCount === 2) { 57 | expect(options).toEqual( 58 | expect.objectContaining({ 59 | requestKey: '2', 60 | fetching: true, 61 | data: { 62 | books: [1, 42, 150], 63 | }, 64 | error: null, 65 | failed: false, 66 | url: '/test/succeeds/json-two', 67 | }) 68 | ); 69 | } else if (renderCount === 3) { 70 | expect(options).toEqual( 71 | expect.objectContaining({ 72 | requestKey: '2', 73 | fetching: false, 74 | data: { 75 | authors: [22, 13], 76 | }, 77 | error: null, 78 | failed: false, 79 | url: '/test/succeeds/json-two', 80 | }) 81 | ); 82 | } else if (renderCount > 3) { 83 | done.fail(); 84 | } 85 | } 86 | }} 87 | 88 | ); 89 | 90 | setTimeout(() => { 91 | // NOTE: this is for adding stuff to the cache. 92 | // This DOES NOT test the cache-only behavior! 93 | expect(fetchMock.calls('/test/succeeds/json-one').length).toBe(1); 94 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 95 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 96 | expect(afterFetchMock).toBeCalledWith( 97 | expect.objectContaining({ 98 | url: '/test/succeeds/json-one', 99 | error: null, 100 | failed: false, 101 | didUnmount: false, 102 | data: { 103 | books: [1, 42, 150], 104 | }, 105 | }) 106 | ); 107 | expect(onResponseMock).toHaveBeenCalledTimes(1); 108 | expect(onResponseMock).toBeCalledWith( 109 | null, 110 | expect.objectContaining({ 111 | ok: true, 112 | status: 200, 113 | statusText: 'OK', 114 | data: { 115 | books: [1, 42, 150], 116 | }, 117 | }) 118 | ); 119 | 120 | run = 2; 121 | wrapper.setProps({ 122 | url: '/test/succeeds/json-two', 123 | requestKey: '2', 124 | }); 125 | 126 | // We do a network timeout here to ensure that the `expect` within 127 | // render is called a second time. 128 | setTimeout(() => { 129 | done(); 130 | }, networkTimeout); 131 | }, networkTimeout); 132 | }); 133 | 134 | test('it uses an indirectly-updated request key on subsequent renders', done => { 135 | // expect.assertions(10); 136 | const onResponseMock = jest.fn(); 137 | const beforeFetchMock = jest.fn(); 138 | const afterFetchMock = jest.fn(); 139 | 140 | let run = 1; 141 | let renderCount = 0; 142 | 143 | const wrapper = mount( 144 | 149 | {options => { 150 | renderCount++; 151 | if (run === 1) { 152 | if (renderCount === 1) { 153 | expect(options).toEqual( 154 | expect.objectContaining({ 155 | fetching: false, 156 | data: null, 157 | error: null, 158 | failed: false, 159 | response: null, 160 | requestName: 'anonymousRequest', 161 | url: '/test/succeeds/json-one', 162 | }) 163 | ); 164 | } else if (renderCount === 2) { 165 | expect(options).toEqual( 166 | expect.objectContaining({ 167 | fetching: true, 168 | data: null, 169 | error: null, 170 | failed: false, 171 | response: null, 172 | requestName: 'anonymousRequest', 173 | url: '/test/succeeds/json-one', 174 | }) 175 | ); 176 | } else if (renderCount === 3) { 177 | expect(options).toEqual( 178 | expect.objectContaining({ 179 | fetching: false, 180 | data: { 181 | books: [1, 42, 150], 182 | }, 183 | error: null, 184 | failed: false, 185 | requestName: 'anonymousRequest', 186 | url: '/test/succeeds/json-one', 187 | }) 188 | ); 189 | } else if (renderCount > 3) { 190 | done.fail(); 191 | } 192 | } else if (run === 2) { 193 | if (renderCount === 1) { 194 | expect(options).toEqual( 195 | expect.objectContaining({ 196 | fetching: false, 197 | data: { 198 | books: [1, 42, 150], 199 | }, 200 | error: null, 201 | failed: false, 202 | requestName: 'anonymousRequest', 203 | url: '/test/succeeds/json-one', 204 | }) 205 | ); 206 | } else if (renderCount === 2) { 207 | expect(options).toEqual( 208 | expect.objectContaining({ 209 | fetching: true, 210 | data: { 211 | books: [1, 42, 150], 212 | }, 213 | error: null, 214 | failed: false, 215 | requestName: 'anonymousRequest', 216 | url: '/test/succeeds/json-two', 217 | }) 218 | ); 219 | } else if (renderCount === 3) { 220 | expect(options).toEqual( 221 | expect.objectContaining({ 222 | fetching: false, 223 | data: { 224 | authors: [22, 13], 225 | }, 226 | error: null, 227 | failed: false, 228 | requestName: 'anonymousRequest', 229 | url: '/test/succeeds/json-two', 230 | }) 231 | ); 232 | } else if (renderCount > 3) { 233 | done.fail(); 234 | } 235 | } else if (run === 3) { 236 | if (renderCount === 1) { 237 | expect(options).toEqual( 238 | expect.objectContaining({ 239 | fetching: false, 240 | data: { 241 | authors: [22, 13], 242 | }, 243 | error: null, 244 | failed: false, 245 | requestName: 'anonymousRequest', 246 | url: '/test/succeeds/json-two', 247 | }) 248 | ); 249 | } else if (renderCount === 2) { 250 | expect(options).toEqual( 251 | expect.objectContaining({ 252 | fetching: false, 253 | data: { 254 | books: [1, 42, 150], 255 | }, 256 | error: null, 257 | failed: false, 258 | requestName: 'anonymousRequest', 259 | url: '/test/succeeds/json-one', 260 | }) 261 | ); 262 | } else if (renderCount > 2) { 263 | done.fail(); 264 | } 265 | } 266 | }} 267 | 268 | ); 269 | 270 | setTimeout(() => { 271 | // NOTE: this is for adding stuff to the cache. 272 | // This DOES NOT test the cache-only behavior! 273 | expect(fetchMock.calls('/test/succeeds/json-one').length).toBe(1); 274 | expect(beforeFetchMock).toHaveBeenCalledTimes(1); 275 | expect(afterFetchMock).toHaveBeenCalledTimes(1); 276 | expect(afterFetchMock).toBeCalledWith( 277 | expect.objectContaining({ 278 | url: '/test/succeeds/json-one', 279 | error: null, 280 | failed: false, 281 | didUnmount: false, 282 | data: { 283 | books: [1, 42, 150], 284 | }, 285 | }) 286 | ); 287 | expect(onResponseMock).toHaveBeenCalledTimes(1); 288 | expect(onResponseMock).toBeCalledWith( 289 | null, 290 | expect.objectContaining({ 291 | ok: true, 292 | status: 200, 293 | statusText: 'OK', 294 | data: { 295 | books: [1, 42, 150], 296 | }, 297 | }) 298 | ); 299 | 300 | run = 2; 301 | renderCount = 0; 302 | wrapper.setProps({ 303 | url: '/test/succeeds/json-two', 304 | }); 305 | 306 | setTimeout(() => { 307 | run = 3; 308 | renderCount = 0; 309 | wrapper.setProps({ 310 | url: '/test/succeeds/json-one', 311 | }); 312 | 313 | setTimeout(() => { 314 | done(); 315 | }, 500); 316 | }, 500); 317 | }, networkTimeout); 318 | }); 319 | }); 320 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch'; 2 | import fetchMock from 'fetch-mock'; 3 | import Enzyme from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import { 6 | successfulResponse, 7 | jsonResponse, 8 | jsonResponse2, 9 | jsonResponse3, 10 | } from './responses'; 11 | 12 | Enzyme.configure({ adapter: new Adapter() }); 13 | 14 | // We need an AbortSignal that can be instantiated without 15 | // an error. 16 | global.AbortSignal = function() {}; 17 | 18 | var hangingPromise = (global.hangingPromise = function() { 19 | return new Promise(() => {}); 20 | }); 21 | 22 | fetchMock.get('/test/hangs', hangingPromise()); 23 | fetchMock.get('/test/hangs/1', hangingPromise()); 24 | fetchMock.get('/test/hangs/2', hangingPromise()); 25 | fetchMock.post('/test/hangs', hangingPromise()); 26 | fetchMock.put('/test/hangs', hangingPromise()); 27 | fetchMock.patch('/test/hangs', hangingPromise()); 28 | fetchMock.head('/test/hangs', hangingPromise()); 29 | fetchMock.delete('/test/hangs', hangingPromise()); 30 | 31 | // This could be improved by adding the URL to the JSON response 32 | fetchMock.get('/test/succeeds', () => { 33 | return new Promise(resolve => { 34 | resolve(jsonResponse()); 35 | }); 36 | }); 37 | 38 | fetchMock.get( 39 | '/test/succeeds/cache-only-empty', 40 | () => 41 | new Promise(resolve => { 42 | resolve(successfulResponse()); 43 | }) 44 | ); 45 | 46 | fetchMock.get( 47 | '/test/succeeds/cache-only-full', 48 | () => 49 | new Promise(resolve => { 50 | resolve(jsonResponse()); 51 | }) 52 | ); 53 | 54 | fetchMock.post( 55 | '/test/succeeds/cache-only-full', 56 | () => 57 | new Promise(resolve => { 58 | resolve(jsonResponse()); 59 | }) 60 | ); 61 | 62 | fetchMock.get( 63 | '/test/succeeds/json-one', 64 | () => 65 | new Promise(resolve => { 66 | resolve(jsonResponse()); 67 | }) 68 | ); 69 | 70 | fetchMock.get( 71 | '/test/succeeds/json-two', 72 | () => 73 | new Promise(resolve => { 74 | resolve(jsonResponse2()); 75 | }) 76 | ); 77 | 78 | fetchMock.patch( 79 | '/test/succeeds/patch', 80 | () => 81 | new Promise(resolve => { 82 | resolve(jsonResponse3()); 83 | }) 84 | ); 85 | 86 | // We do this at the start of each test, just in case a test 87 | // replaces the global fetch and does not reset it 88 | beforeEach(() => { 89 | fetchMock.reset(); 90 | }); 91 | --------------------------------------------------------------------------------