Skip to content

Commit dc51a46

Browse files
authored
retryOn, retryDelay, enhancements, and bugfixes/docs updates (#218)
* init * fix merge conflicts * wip * c * docs * docs cleanup * docs cleanup * retryOn & retryDelay with values and functions are implemented * docs cleanup * added boilerplate tests, fixed types, alphabetize options * alphabetize useFetch options * fixing current tests * added tests, fixed bugs, put todos in for error tests (issue with react-hooks-testing-library for testing errors) * alphabetizing options in docs * added tests * updated package.json deps, fixed console hack, got testing working for console.error * adding codepen examples to readme * cleanup types, makeError * test for no content-type: application/json on post with FormData * fixed pagination test, added tests for request rejections * fix lint errors * fixing tests, updating docs * testing to see if it centers browser support * nope, it does not * Update README.md * Update README.md * only 1 rerender when pulling from cache, memoized `response` so it doesnt cause infinite loop when used as dependency * small bug fix * fixing suspense tests
1 parent 1d42e23 commit dc51a46

File tree

11 files changed

+1480
-1155
lines changed

11 files changed

+1480
-1155
lines changed

README.md

Lines changed: 154 additions & 124 deletions
Large diffs are not rendered by default.

config/setupTests.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,3 @@ import { GlobalWithFetchMock } from 'jest-fetch-mock'
33
const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock
44
customGlobal.fetch = require('jest-fetch-mock')
55
customGlobal.fetchMock = customGlobal.fetch
6-
7-
// this is just a little hack to silence a warning that we'll get until react
8-
// fixes this: https://github.com/facebook/react/pull/14853
9-
const originalError = console.error
10-
beforeAll(() => {
11-
console.error = (...args: any[]) => {
12-
if (/Warning.*not wrapped in act/.test(args[0])) {
13-
return
14-
}
15-
originalError.call(console, ...args)
16-
}
17-
})
18-
19-
afterAll(() => {
20-
console.error = originalError
21-
})

docs/README.md

Lines changed: 118 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,18 @@ Features
6161
- Built in caching
6262
- Persistent caching support
6363
- Suspense<sup>(experimental)</sup> support
64+
- Retry functionality
6465

6566
Examples
6667
=========
6768

6869
- <a target="_blank" rel="noopener noreferrer" href='https://codesandbox.io/s/usefetch-in-nextjs-nn9fm'>useFetch + Next.js</a>
6970
- <a target="_blank" rel="noopener noreferrer" href='https://codesandbox.io/embed/km04k9k9x5'>useFetch + create-react-app</a>
7071
- <a target="_blank" rel="noopener noreferrer" href='https://codesandbox.io/s/usefetch-with-provider-c78w2'>useFetch + Provider</a>
71-
- <li><a target="_blank" rel="noopener noreferrer" href='https://codesandbox.io/s/usefetch-suspense-i22wv'>useFetch + Suspense</a></li>
72+
- <a target="_blank" rel="noopener noreferrer" href='https://codesandbox.io/s/usefetch-suspense-i22wv'>useFetch + Suspense</a>
7273
- <a target="_blank" rel="noopener noreferrer" href='https://codesandbox.io/s/usefetch-provider-pagination-exttg'>useFetch + Pagination + Provider</a>
7374
- <a target="_blank" rel="noopener noreferrer" href='https://codesandbox.io/s/usefetch-provider-requestresponse-interceptors-s1lex'>useFetch + Request/Response Interceptors + Provider</a>
75+
- <a target="_blank" rel="noopener noreferrer" href='https://codesandbox.io/s/usefetch-retryon-retrydelay-s74q9'>useFetch + retryOn, retryDelay</a>
7476
- <a target="_blank" rel="noopener noreferrer" href='https://codesandbox.io/s/graphql-usequery-provider-uhdmj'>useQuery - GraphQL</a>
7577

7678
Installation
@@ -574,6 +576,46 @@ const App = () => {
574576
}
575577
```
576578
579+
Retries
580+
-------
581+
582+
In this example you can see how `retryOn` will retry on a status code of `305`, or if we choose the `retryOn()` function, it returns a boolean to decide if we will retry. With `retryDelay` we can either have a fixed delay, or a dynamic one by using `retryDelay()`. Make sure `retries` is set to at minimum `1` otherwise it won't retry the request. If `retries > 0` without `retryOn` then by default we always retry if there's an error or if `!response.ok`. If `retryOn: [400]` and `retries > 0` then we only retry on a response status of `400`, not on any generic network error.
583+
584+
```js
585+
import useFetch from 'use-http'
586+
587+
const TestRetry = () => {
588+
const { response, get } = useFetch('https://httpbin.org/status/305', {
589+
// make sure `retries` is set otherwise it won't retry
590+
retries: 1,
591+
retryOn: [305],
592+
// OR
593+
retryOn: ({ attempt, error, response }) => {
594+
// returns true or false to determine whether to retry
595+
return error || response && response.status >= 300
596+
},
597+
598+
retryDelay: 3000,
599+
// OR
600+
retryDelay: ({ attempt, error, response }) => {
601+
// exponential backoff
602+
return Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)
603+
// linear backoff
604+
return attempt * 1000
605+
}
606+
})
607+
608+
return (
609+
<>
610+
<button onClick={() => get()}>CLICK</button>
611+
<pre>{JSON.stringify(response, null, 2)}</pre>
612+
</>
613+
)
614+
}
615+
```
616+
617+
[![Edit Basic Example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/usefetch-retryon-retrydelay-s74q9)
618+
577619
GraphQL Query
578620
---------------
579621
@@ -720,8 +762,8 @@ function App() {
720762
Hooks
721763
=======
722764
723-
| Option | Description |
724-
| --------------------- | ---------------------------------------------------------------------------------------- |
765+
| Option | Description |
766+
| --------------------- | ------------------ |
725767
| `useFetch` | The base hook |
726768
| `useQuery` | For making a GraphQL query |
727769
| `useMutation` | For making a GraphQL mutation |
@@ -733,79 +775,109 @@ This is exactly what you would pass to the normal js `fetch`, with a little extr
733775
734776
| Option | Description | Default |
735777
| --------------------- | --------------------------------------------------------------------------|------------- |
736-
| `suspense` | Enables React Suspense mode. [example](https://codesandbox.io/s/usefetch-suspense-i22wv) | false |
737-
| `cachePolicy` | These will be the same ones as Apollo's [fetch policies](https://www.apollographql.com/docs/react/api/react-apollo/#optionsfetchpolicy). Possible values are `cache-and-network`, `network-only`, `cache-only`, `no-cache`, `cache-first`. Currently only supports **`cache-first`** or **`no-cache`** | `cache-first` |
738778
| `cacheLife` | After a successful cache update, that cache data will become stale after this duration | `0` |
739-
| `url` | Allows you to set a base path so relative paths can be used for each request :) | empty string |
740-
| `onNewData` | Merges the current data with the incoming data. Great for pagination. | `(curr, new) => new` |
741-
| `perPage` | Stops making more requests if there is no more data to fetch. (i.e. if we have 25 todos, and the perPage is 10, after fetching 2 times, we will have 20 todos. The last 5 tells us we don't have any more to fetch because it's less than 10) For pagination. | `0` |
742-
| `onAbort` | Runs when the request is aborted. | empty function |
743-
| `onTimeout` | Called when the request times out. | empty function |
744-
| `retries` | When a request fails or times out, retry the request this many times. By default it will not retry. | `0` |
745-
| `timeout` | The request will be aborted/cancelled after this amount of time. This is also the interval at which `retries` will be made at. **in milliseconds** | `30000` </br> (30 seconds) |
779+
| `cachePolicy` | These will be the same ones as Apollo's [fetch policies](https://www.apollographql.com/docs/react/api/react-apollo/#optionsfetchpolicy). Possible values are `cache-and-network`, `network-only`, `cache-only`, `no-cache`, `cache-first`. Currently only supports **`cache-first`** or **`no-cache`** | `cache-first` |
746780
| `data` | Allows you to set a default value for `data` | `undefined` |
747-
| `loading` | Allows you to set default value for `loading` | `false` unless the last argument of `useFetch` is `[]` |
748781
| `interceptors.request` | Allows you to do something before an http request is sent out. Useful for authentication if you need to refresh tokens a lot. | `undefined` |
749782
| `interceptors.response` | Allows you to do something after an http response is recieved. Useful for something like camelCasing the keys of the response. | `undefined` |
783+
| `loading` | Allows you to set default value for `loading` | `false` unless the last argument of `useFetch` is `[]` |
784+
| `onAbort` | Runs when the request is aborted. | empty function |
785+
| `onNewData` | Merges the current data with the incoming data. Great for pagination. | `(curr, new) => new` |
786+
| `onTimeout` | Called when the request times out. | empty function |
787+
| `path` | When using a global `url` set in the `Provider`, this is useful for adding onto it | `''` |
750788
| `persist` | Persists data for the duration of `cacheLife`. If `cacheLife` is not set it defaults to 24h. Currently only available in Browser. | `false` |
789+
| `perPage` | Stops making more requests if there is no more data to fetch. (i.e. if we have 25 todos, and the perPage is 10, after fetching 2 times, we will have 20 todos. The last 5 tells us we don't have any more to fetch because it's less than 10) For pagination. | `0` |
790+
| `retries` | When a request fails or times out, retry the request this many times. By default it will not retry. | `0` |
791+
| `retryDelay` | You can retry with certain intervals i.e. 30 seconds `30000` or with custom logic (i.e. to increase retry intervals). | `1000` |
792+
| `retryOn` | You can retry on certain http status codes or have custom logic to decide whether to retry or not via a function. Make sure `retries > 0` otherwise it won't retry. | `[]` |
793+
| `suspense` | Enables Experimental React Suspense mode. [example](https://codesandbox.io/s/usefetch-suspense-i22wv) | `false` |
794+
| `timeout` | The request will be aborted/cancelled after this amount of time. This is also the interval at which `retries` will be made at. **in milliseconds**. If set to `0`, it will not timeout except for browser defaults. | `0` |
795+
| `url` | Allows you to set a base path so relative paths can be used for each request :) | empty string |
751796
752797
```jsx
753798
const options = {
754799
// accepts all `fetch` options such as headers, method, etc.
755-
756-
// enables React Suspense mode
757-
suspense: true, // defaults to `false`
800+
801+
// The time in milliseconds that cache data remains fresh.
802+
cacheLife: 0,
758803

759804
// Cache responses to improve speed and reduce amount of requests
760805
// Only one request to the same endpoint will be initiated unless cacheLife expires for 'cache-first'.
761806
cachePolicy: 'cache-first' // 'no-cache'
807+
808+
// set's the default for the `data` field
809+
data: [],
762810

763-
// The time in milliseconds that cache data remains fresh.
764-
cacheLife: 0,
765-
766-
// Allows caching to persist after page refresh. Only supported in the Browser currently.
767-
persist: false,
811+
// typically, `interceptors` would be added as an option to the `<Provider />`
812+
interceptors: {
813+
request: async (options, url, path, route) => { // `async` is not required
814+
return options // returning the `options` is important
815+
},
816+
response: async (response) => {
817+
// note: `response.data` is equivalent to `await response.json()`
818+
return response // returning the `response` is important
819+
}
820+
},
768821

769-
// used to be `baseUrl`. You can set your URL this way instead of as the 1st argument
770-
url: 'https://example.com',
771-
772-
// called when the request times out
773-
onTimeout: () => {},
822+
// set's the default for `loading` field
823+
loading: false,
774824

775825
// called when aborting the request
776826
onAbort: () => {},
777827

778-
// this will allow you to merge the data however you choose. Used for Pagination
828+
// this will allow you to merge the `data` for pagination.
779829
onNewData: (currData, newData) => {
780830
return [...currData, ...newData]
781831
},
782832

833+
// called when the request times out
834+
onTimeout: () => {},
835+
836+
// if you have a global `url` set up, this is how you can add to it
837+
path: '/path/to/your/api',
838+
783839
// this will tell useFetch not to run the request if the list doesn't haveMore. (pagination)
784840
// i.e. if the last page fetched was < 15, don't run the request again
785841
perPage: 15,
786842

843+
// Allows caching to persist after page refresh. Only supported in the Browser currently.
844+
persist: false,
845+
787846
// amount of times it should retry before erroring out
788847
retries: 3,
848+
849+
// The time between retries
850+
retryDelay: 10000,
851+
// OR
852+
// Can be a function which is used if we want change the time in between each retry
853+
retryDelay({ attempt, error, response }) {
854+
// exponential backoff
855+
return Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)
856+
// linear backoff
857+
return attempt * 1000
858+
},
859+
860+
861+
// make sure `retries` is set otherwise it won't retry
862+
// can retry on certain http status codes
863+
retryOn: [503],
864+
// OR
865+
retryOn({ attempt, error, response }) {
866+
// retry on any network error, or 4xx or 5xx status codes
867+
if (error !== null || response.status >= 400) {
868+
console.log(`retrying, attempt number ${attempt + 1}`);
869+
return true;
870+
}
871+
},
872+
873+
// enables experimental React Suspense mode
874+
suspense: true, // defaults to `false`
789875

790-
// amount of time before the request (or request(s) for each retry) errors out.
876+
// amount of time before the request get's canceled/aborted
791877
timeout: 10000,
792-
793-
// set's the default for the `data` field
794-
data: [],
795-
796-
// set's the default for `loading` field
797-
loading: false,
798-
799-
// typically, `interceptors` would be added as an option to the `<Provider />`
800-
interceptors: {
801-
request: async (options, url, path, route) => { // `async` is not required
802-
return options // returning the `options` is important
803-
},
804-
response: async (response) => { // `async` is not required
805-
// note: `response.data` is equivalent to `await response.json()`
806-
return response // returning the `response` is important
807-
}
808-
}
878+
879+
// used to be `baseUrl`. You can set your URL this way instead of as the 1st argument
880+
url: 'https://example.com',
809881
}
810882

811883
useFetch(options)
@@ -883,4 +955,4 @@ const App = () => {
883955
[2]: https://github.com/alex-cory/use-http/issues/93#issuecomment-600896722
884956
[3]: https://github.com/alex-cory/use-http/raw/master/public/dog.png
885957
[4]: https://reactjs.org/docs/javascript-environment-requirements.html
886-
[`react-app-polyfill`]: https://www.npmjs.com/package/react-app-polyfill
958+
[`react-app-polyfill`]: https://www.npmjs.com/package/react-app-polyfill

package.json

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,42 @@
1212
"use-ssr": "^1.0.22"
1313
},
1414
"peerDependencies": {
15-
"react": "^16.13.0",
16-
"react-dom": "^16.13.0"
15+
"react": "^16.13.1",
16+
"react-dom": "^16.13.1"
1717
},
1818
"devDependencies": {
19-
"@testing-library/react": "^10.0.0",
20-
"@testing-library/react-hooks": "^3.0.0",
21-
"@types/fetch-mock": "^7.2.3",
22-
"@types/jest": "^25.1.0",
23-
"@types/node": "^13.9.0",
24-
"@types/react": "^16.9.23",
25-
"@types/react-dom": "^16.8.4",
26-
"@typescript-eslint/eslint-plugin": "^2.23.0",
27-
"@typescript-eslint/parser": "^2.23.0",
19+
"@testing-library/react": "^10.0.2",
20+
"@testing-library/react-hooks": "^3.2.1",
21+
"@types/fetch-mock": "^7.3.2",
22+
"@types/jest": "^25.1.4",
23+
"@types/node": "^13.9.8",
24+
"@types/react": "^16.9.30",
25+
"@types/react-dom": "^16.9.5",
26+
"@typescript-eslint/eslint-plugin": "^2.26.0",
27+
"@typescript-eslint/parser": "^2.26.0",
2828
"convert-keys": "^1.3.4",
2929
"eslint": "^6.8.0",
30-
"eslint-config-standard": "^14.1.0",
31-
"eslint-plugin-import": "^2.20.1",
32-
"eslint-plugin-jest": "23.8.2",
30+
"eslint-config-standard": "^14.1.1",
31+
"eslint-plugin-import": "^2.20.2",
32+
"eslint-plugin-jest": "^23.8.2",
3333
"eslint-plugin-jest-formatting": "^1.2.0",
34-
"eslint-plugin-jsx-a11y": "^6.2.1",
35-
"eslint-plugin-node": "^11.0.0",
34+
"eslint-plugin-jsx-a11y": "^6.2.3",
35+
"eslint-plugin-node": "^11.1.0",
3636
"eslint-plugin-promise": "^4.2.1",
3737
"eslint-plugin-react": "^7.19.0",
38-
"eslint-plugin-react-hooks": "^2.5.0",
38+
"eslint-plugin-react-hooks": "^3.0.0",
3939
"eslint-plugin-standard": "^4.0.1",
4040
"eslint-watch": "^6.0.1",
41-
"jest": "^25.1.0",
41+
"jest": "^25.2.4",
4242
"jest-fetch-mock": "^3.0.3",
43+
"jest-mock-console": "^1.0.0",
4344
"mockdate": "^2.0.5",
44-
"react": "^16.8.6",
45-
"react-dom": "^16.8.6",
45+
"react": "^16.13.1",
46+
"react-dom": "^16.13.1",
4647
"react-hooks-testing-library": "^0.6.0",
47-
"react-test-renderer": "^16.8.6",
48-
"ts-jest": "^25.2.1",
49-
"typescript": "^3.4.5",
48+
"react-test-renderer": "^16.13.1",
49+
"ts-jest": "^25.3.0",
50+
"typescript": "^3.8.3",
5051
"utility-types": "^3.10.0",
5152
"watch": "^1.0.2"
5253
},

0 commit comments

Comments
 (0)