diff --git a/README.md b/README.md
index 62fc693f..d327b2ff 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -89,7 +89,6 @@ Usage
### Examples + Videos
- useFetch - managed state, request, response, etc. [![](https://img.shields.io/badge/example-blue.svg)](https://codesandbox.io/s/usefetch-request-response-managed-state-ruyi3?file=/src/index.js) [![](https://img.shields.io/badge/video-red.svg)](https://www.youtube.com/watch?v=_-GujYZFCKI&list=PLZIwrWkE9rCdUybd8t3tY-mUMvXkCdenW&index=6)
-- useFetch - route, path, Provider, etc. [![](https://img.shields.io/badge/example-blue.svg)](https://codesandbox.io/s/usefetch-with-provider-c78w2) [![](https://img.shields.io/badge/video-red.svg)](https://www.youtube.com/watch?v=JWDL_AVOYT0&list=PLZIwrWkE9rCdUybd8t3tY-mUMvXkCdenW&index=10)
- useFetch - request/response interceptors [![](https://img.shields.io/badge/example-blue.svg)](https://codesandbox.io/s/usefetch-provider-requestresponse-interceptors-s1lex) [![](https://img.shields.io/badge/video-red.svg)](https://www.youtube.com/watch?v=3HauoWh0Jts&list=PLZIwrWkE9rCdUybd8t3tY-mUMvXkCdenW&index=8)
- useFetch - retries, retryOn, retryDelay [![](https://img.shields.io/badge/example-blue.svg)](https://codesandbox.io/s/usefetch-retryon-retrydelay-s74q9) [![](https://img.shields.io/badge/video-red.svg)](https://www.youtube.com/watch?v=grE3AX-Q9ss&list=PLZIwrWkE9rCdUybd8t3tY-mUMvXkCdenW&index=9)
- useFetch - abort, timeout, onAbort, onTimeout [![](https://img.shields.io/badge/video-red.svg)](https://www.youtube.com/watch?v=7SuD3ZOfu7E&list=PLZIwrWkE9rCdUybd8t3tY-mUMvXkCdenW&index=4)
@@ -101,7 +100,7 @@ Usage
- useFetch - Next.js [![](https://img.shields.io/badge/example-blue.svg)](https://codesandbox.io/s/usefetch-in-nextjs-nn9fm)
- useFetch - create-react-app [![](https://img.shields.io/badge/example-blue.svg)](https://codesandbox.io/embed/km04k9k9x5)
-Basic Usage (managed state) useFetch
+Basic Usage Managed State useFetch
If the last argument of `useFetch` is not a dependency array `[]`, then it will not fire until you call one of the http methods like `get`, `post`, etc.
@@ -147,20 +146,18 @@ function Todos() {
-Basic Usage (auto managed state) useFetch
+Basic Usage Auto-Managed State useFetch
-This fetch is run `onMount/componentDidMount`. The last argument `[]` means it will run `onMount`. If you pass it a variable like `[someVariable]`, it will run `onMount` and again whenever `someVariable` changes values (aka `onUpdate`). **If no method is specified, GET is the default**
+This fetch is run `onMount/componentDidMount`. The last argument `[]` means it will run `onMount`. If you pass it a variable like `[someVariable]`, it will run `onMount` and again whenever `someVariable` changes values (aka `onUpdate`). If no method is specified, GET is the default.
```js
import useFetch from 'use-http'
function Todos() {
- // accepts all `fetch` options
- const options = {
- data: [], // setting default for `data` as array instead of undefined
- }
-
- const { loading, error, data } = useFetch('https://example.com/todos', options, []) // onMount (GET by default)
+ const { loading, error, data } = useFetch('https://example.com/todos', {
+ // these options accept all native `fetch` options
+ data: [] // defaults the `data` to an array instead of `undefined`
+ }, []) // <- this [] means it will fire onMount (GET by default)
return (
<>
@@ -179,43 +176,7 @@ function Todos() {
-
-Basic Usage (auto managed state) with Provider
-
-```js
-import useFetch, { Provider } from 'use-http'
-
-function Todos() {
- const { loading, error, data } = useFetch({
- path: '/todos',
- data: []
- }, []) // onMount
-
- return (
- <>
- {error && 'Error!'}
- {loading && 'Loading...'}
- {data.map(todo => (
- {todo.title}
- )}
- >
- )
-}
-
-const App = () => (
-
-
-
-)
-```
-
-
-
-
-
-
-
-Suspense Mode (auto managed state)
+Suspense Mode(experimental) Auto-Managed State
Can put `suspense` in 2 places. Either `useFetch` (A) or `Provider` (B).
@@ -223,8 +184,7 @@ Can put `suspense` in 2 places. Either `useFetch` (A) or `Provider` (B).
import useFetch, { Provider } from 'use-http'
function Todos() {
- const { data: todos } = useFetch({
- path: '/todos',
+ const { data: todos } = useFetch('/todos', {
data: [],
suspense: true // A. can put `suspense: true` here
}, []) // onMount
@@ -250,9 +210,9 @@ function App() {
-Suspense Mode (managed state)
+Suspense Mode(experimental) Managed State
-Can put `suspense` in 2 places. Either `useFetch` (A) or `Provider` (B).
+Can put `suspense` in 2 places. Either `useFetch` (A) or `Provider` (B). Suspense mode via managed state is very experimental.
```js
import useFetch, { Provider } from 'use-http'
@@ -328,8 +288,7 @@ import useFetch, { Provider } from 'use-http'
const Todos = () => {
const [page, setPage] = useState(1)
- const { data, loading } = useFetch({
- path: `/todos?page=${page}&amountPerPage=15`,
+ const { data, loading } = useFetch(`/todos?page=${page}&amountPerPage=15`, {
onNewData: (currTodos, newTodos) => [...currTodos, ...newTodos], // appends newly fetched todos
perPage: 15, // stops making more requests if last todos fetched < 15
data: []
@@ -430,10 +389,7 @@ var {
Relative routes useFetch
-⚠️ `baseUrl` is no longer supported, it is now only `url`
```jsx
-var request = useFetch({ url: 'https://example.com' })
-// OR
var request = useFetch('https://example.com')
request.post('/todos', {
@@ -451,17 +407,15 @@ request.post('/todos', {
```jsx
-const githubRepos = useFetch({
- url: `https://api.github.com/search/repositories?q=`
-})
+const { get, abort, loading, data: repos } = useFetch('https://api.github.com/search/repositories?q=')
// the line below is not isomorphic, but for simplicity we're using the browsers `encodeURI`
-const searchGithubRepos = e => githubRepos.get(encodeURI(e.target.value))
+const searchGithubRepos = e => get(encodeURI(e.target.value))
<>
- Abort
- {githubRepos.loading ? 'Loading...' : githubRepos.data.items.map(repo => (
+ Abort
+ {loading ? 'Loading...' : repos.data.items.map(repo => (
{repo.name}
))}
>
@@ -853,7 +807,6 @@ This is exactly what you would pass to the normal js `fetch`, with a little extr
| `onError` | Runs when the request get's an error. If retrying, it is only called on the last retry attempt. | empty function |
| `onNewData` | Merges the current data with the incoming data. Great for pagination. | `(curr, new) => new` |
| `onTimeout` | Called when the request times out. | empty function |
-| `path` | When using a global `url` set in the `Provider`, this is useful for adding onto it | `''` |
| `persist` | Persists data for the duration of `cacheLife`. If `cacheLife` is not set it defaults to 24h. Currently only available in Browser. | `false` |
| `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` |
| `responseType` | This will determine how the `data` field is set. If you put `json` then it will try to parse it as JSON. If you set it as an array, it will attempt to parse the `response` in the order of the types you put in the array. Read about why we don't put `formData` in the defaults [in the yellow Note part here](https://developer.mozilla.org/en-US/docs/Web/API/Body/formData). | `['json', 'text', 'blob', 'readableStream']` |
@@ -862,7 +815,6 @@ This is exactly what you would pass to the normal js `fetch`, with a little extr
| `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. | `[]` |
| `suspense` | Enables Experimental React Suspense mode. [example](https://codesandbox.io/s/usefetch-suspense-i22wv) | `false` |
| `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` |
-| `url` | Allows you to set a base path so relative paths can be used for each request :) | empty string |
```jsx
const options = {
@@ -906,9 +858,6 @@ const options = {
// called when the request times out
onTimeout: () => {},
- // if you have a global `url` set up, this is how you can add to it
- path: '/path/to/your/api',
-
// this will tell useFetch not to run the request if the list doesn't haveMore. (pagination)
// i.e. if the last page fetched was < 15, don't run the request again
perPage: 15,
@@ -957,9 +906,6 @@ const options = {
// amount of time before the request get's canceled/aborted
timeout: 10000,
-
- // used to be `baseUrl`. You can set your URL this way instead of as the 1st argument
- url: 'https://example.com',
}
useFetch(options)
@@ -1001,6 +947,9 @@ If you have feature requests, [submit an issue][1] to let us know what you would
Todos
------
+- [ ] prefetching
+- [ ] global cache state management
+- [ ] optimistic updates
- [ ] `persist` support for React Native
- [ ] better loading state management. When using only 1 useFetch in a component and we use
`Promise.all([get('/todos/1'), get('/todos/2')])` then don't have a loading true,
diff --git a/docs/README.md b/docs/README.md
index cd21021c..f9902464 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -5,7 +5,7 @@
🐶 React hook for making isomorphic http requests
-
+
@@ -67,7 +67,6 @@ Examples + Videos
=========
- useFetch - managed state, request, response, etc. [![](https://img.shields.io/badge/example-blue.svg)](https://codesandbox.io/s/usefetch-request-response-managed-state-ruyi3?file=/src/index.js) [![](https://img.shields.io/badge/video-red.svg)](https://www.youtube.com/watch?v=_-GujYZFCKI&list=PLZIwrWkE9rCdUybd8t3tY-mUMvXkCdenW&index=6)
-- useFetch - route, path, Provider, etc. [![](https://img.shields.io/badge/example-blue.svg)](https://codesandbox.io/s/usefetch-with-provider-c78w2) [![](https://img.shields.io/badge/video-red.svg)](https://www.youtube.com/watch?v=JWDL_AVOYT0&list=PLZIwrWkE9rCdUybd8t3tY-mUMvXkCdenW&index=10)
- useFetch - request/response interceptors [![](https://img.shields.io/badge/example-blue.svg)](https://codesandbox.io/s/usefetch-provider-requestresponse-interceptors-s1lex) [![](https://img.shields.io/badge/video-red.svg)](https://www.youtube.com/watch?v=3HauoWh0Jts&list=PLZIwrWkE9rCdUybd8t3tY-mUMvXkCdenW&index=8)
- useFetch - retries, retryOn, retryDelay [![](https://img.shields.io/badge/example-blue.svg)](https://codesandbox.io/s/usefetch-retryon-retrydelay-s74q9) [![](https://img.shields.io/badge/video-red.svg)](https://www.youtube.com/watch?v=grE3AX-Q9ss&list=PLZIwrWkE9rCdUybd8t3tY-mUMvXkCdenW&index=9)
- useFetch - abort, timeout, onAbort, onTimeout [![](https://img.shields.io/badge/video-red.svg)](https://www.youtube.com/watch?v=7SuD3ZOfu7E&list=PLZIwrWkE9rCdUybd8t3tY-mUMvXkCdenW&index=4)
@@ -115,20 +114,19 @@ yarn add use-http or npm i -S use-http
Usage
=============
-Basic Usage (auto managed state)
+Basic Usage Auto-Managed State
-------------------
-This fetch is run `onMount/componentDidMount`. The last argument `[]` means it will run `onMount`. If you pass it a variable like `[someVariable]`, it will run `onMount` and again whenever `someVariable` changes values (aka `onUpdate`). **If no method is specified, GET is the default**
+This fetch is run `onMount/componentDidMount`. The last argument `[]` means it will run `onMount`. If you pass it a variable like `[someVariable]`, it will run `onMount` and again whenever `someVariable` changes values (aka `onUpdate`). If no method is specified, GET is the default.
```js
import useFetch from 'use-http'
function Todos() {
- const options = { // accepts all `fetch` options
- data: [] // default for `data` will be an array instead of undefined
- }
-
- const { loading, error, data } = useFetch('https://example.com/todos', options, []) // onMount (GET by default)
+ const { loading, error, data } = useFetch('https://example.com/todos', {
+ // these options accept all native `fetch` options
+ data: [] // defaults the `data` to an array instead of `undefined`
+ }, []) // <- this [] means it will fire onMount (GET by default)
return (
<>
@@ -190,50 +188,14 @@ function Todos() {
-Basic Usage With Provider (auto managed state)
----------------------------------------------
-
-```js
-import useFetch, { Provider } from 'use-http'
-
-function Todos() {
- const { loading, error, data } = useFetch({
- path: '/todos',
- data: [] // default for `data` will be an array instead of undefined
- }, []) // onMount
-
- return (
- <>
- {error && 'Error!'}
- {loading && 'Loading...'}
- {data.map(todo => (
-
{todo.title}
- )}
- >
- )
-}
-
-const App = () => (
-
-
-
-)
-```
-
-
-
-
-
-
-Suspense Mode (auto managed state)
+Suspense Mode Auto-Managed State
----------------------------------
```js
import useFetch, { Provider } from 'use-http'
function Todos() {
- const { data: todos } = useFetch({
- path: '/todos',
+ const { data: todos } = useFetch('/todos', {
data: [],
suspense: true // can put it in 2 places. Here or in Provider
}, []) // onMount
@@ -257,7 +219,7 @@ function App() {
-Suspense Mode (managed state)
+Suspense Mode Managed State
-----------------------------
Can put `suspense` in 2 places. Either `useFetch` (A) or `Provider` (B).
@@ -309,8 +271,7 @@ import useFetch, { Provider } from 'use-http'
const Todos = () => {
const [page, setPage] = useState(1)
- const { data, loading } = useFetch({
- path: `/todos?page=${page}&amountPerPage=15`,
+ const { data, loading } = useFetch(`/todos?page=${page}&amountPerPage=15`, {
onNewData: (currTodos, newTodos) => [...currTodos, ...newTodos], // appends newly fetched todos
perPage: 15, // stops making more requests if last todos fetched < 15
data: []
@@ -407,11 +368,7 @@ var {
Relative routes
---------------
-⚠️ `baseUrl` is no longer supported, it is now only `url`
-
```js
-var request = useFetch({ url: 'https://example.com' })
-// OR
var request = useFetch('https://example.com')
request.post('/todos', {
@@ -427,17 +384,15 @@ Abort
```js
-const githubRepos = useFetch({
- url: `https://api.github.com/search/repositories?q=`
-})
+const { get, abort, loading, data: repos } = useFetch('https://api.github.com/search/repositories?q=')
// the line below is not isomorphic, but for simplicity we're using the browsers `encodeURI`
-const searchGithubRepos = e => githubRepos.get(encodeURI(e.target.value))
+const searchGithubRepos = e => get(encodeURI(e.target.value))
<>
- Abort
- {githubRepos.loading ? 'Loading...' : githubRepos.data.items.map(repo => (
+ Abort
+ {loading ? 'Loading...' : repos.data.items.map(repo => (
{repo.name}
))}
>
@@ -804,7 +759,6 @@ This is exactly what you would pass to the normal js `fetch`, with a little extr
| `onError` | Runs when the request get's an error. If retrying, it is only called on the last retry attempt. | empty function |
| `onNewData` | Merges the current data with the incoming data. Great for pagination. | `(curr, new) => new` |
| `onTimeout` | Called when the request times out. | empty function |
-| `path` | When using a global `url` set in the `Provider`, this is useful for adding onto it | `''` |
| `persist` | Persists data for the duration of `cacheLife`. If `cacheLife` is not set it defaults to 24h. Currently only available in Browser. | `false` |
| `responseType` | This will determine how the `data` field is set. If you put `json` then it will try to parse it as JSON. If you set it as an array, it will attempt to parse the `response` in the order of the types you put in the array. Read about why we don't put `formData` in the defaults [in the yellow Note part here](https://developer.mozilla.org/en-US/docs/Web/API/Body/formData). | `['json', 'text', 'blob', 'readableStream']` |
| `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` |
@@ -813,7 +767,6 @@ This is exactly what you would pass to the normal js `fetch`, with a little extr
| `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. | `[]` |
| `suspense` | Enables Experimental React Suspense mode. [example](https://codesandbox.io/s/usefetch-suspense-i22wv) | `false` |
| `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` |
-| `url` | Allows you to set a base path so relative paths can be used for each request :) | empty string |
```jsx
const options = {
@@ -857,9 +810,6 @@ const options = {
// called when the request times out
onTimeout: () => {},
- // if you have a global `url` set up, this is how you can add to it
- path: '/path/to/your/api',
-
// this will tell useFetch not to run the request if the list doesn't haveMore. (pagination)
// i.e. if the last page fetched was < 15, don't run the request again
perPage: 15,
@@ -909,9 +859,6 @@ const options = {
// amount of time before the request get's canceled/aborted
timeout: 10000,
-
- // used to be `baseUrl`. You can set your URL this way instead of as the 1st argument
- url: 'https://example.com',
}
useFetch(options)
diff --git a/src/__tests__/doFetchArgs.test.tsx b/src/__tests__/doFetchArgs.test.tsx
index 55d13dc8..76cea4c1 100644
--- a/src/__tests__/doFetchArgs.test.tsx
+++ b/src/__tests__/doFetchArgs.test.tsx
@@ -18,12 +18,12 @@ describe('doFetchArgs: general usages', (): void => {
})
const { url, options } = await doFetchArgs(
{},
- '',
- '',
HTTPMethod.POST,
controller,
defaults.cacheLife,
cache,
+ '',
+ '',
expectedRoute,
{}
)
@@ -47,12 +47,12 @@ describe('doFetchArgs: general usages', (): void => {
})
const { options, url } = await doFetchArgs(
{},
- 'https://example.com',
- '',
HTTPMethod.POST,
controller,
defaults.cacheLife,
cache,
+ 'https://example.com',
+ '',
'/test',
[]
)
@@ -76,12 +76,12 @@ describe('doFetchArgs: general usages', (): void => {
})
const { url } = await doFetchArgs(
{},
- 'https://example.com',
- '/path',
HTTPMethod.POST,
controller,
defaults.cacheLife,
cache,
+ 'https://example.com',
+ '/path',
'/route',
{}
)
@@ -103,12 +103,12 @@ describe('doFetchArgs: general usages', (): void => {
}
const { options } = await doFetchArgs(
{},
- '',
- '',
HTTPMethod.POST,
controller,
defaults.cacheLife,
cache,
+ undefined,
+ '',
'/test',
{},
interceptors.request
@@ -154,12 +154,12 @@ describe('doFetchArgs: Errors', (): void => {
await expect(
doFetchArgs(
{},
- '',
- '',
HTTPMethod.GET,
controller,
defaults.cacheLife,
cache,
+ '',
+ '',
{},
{}
)
@@ -196,12 +196,12 @@ describe('doFetchArgs: Errors', (): void => {
await expect(
doFetchArgs(
{},
- '',
- '',
HTTPMethod.GET,
controller,
defaults.cacheLife,
cache,
+ '',
+ '',
[],
[]
)
diff --git a/src/__tests__/useFetch.test.tsx b/src/__tests__/useFetch.test.tsx
index 128ab646..daba0154 100644
--- a/src/__tests__/useFetch.test.tsx
+++ b/src/__tests__/useFetch.test.tsx
@@ -12,8 +12,8 @@ import mockConsole from 'jest-mock-console'
import * as mockdate from 'mockdate'
import defaults from '../defaults'
-import { Res, Options, CachePolicies } from '../types'
-import { emptyCustomResponse, sleep, makeError } from '../utils'
+import { Res, IncomingOptions, CachePolicies } from '../types'
+import { emptyCustomResponse, sleep, makeError, addSlash } from '../utils'
const fetch = global.fetch as FetchMock
@@ -132,6 +132,22 @@ describe('useFetch - BROWSER - basic functionality', (): void => {
})
})
+describe('useFetch - handling host/path/route parsing properly', (): void => {
+ it ('should have addSlash run properly', (): void => {
+ expect(addSlash('', '')).toBe('')
+ expect(addSlash('')).toBe('')
+ expect(addSlash('?foo=bar', 'a.com')).toBe('?foo=bar')
+ expect(addSlash('?foo=bar', 'a.com/')).toBe('?foo=bar')
+ expect(addSlash('?foo=bar')).toBe('?foo=bar')
+ expect(addSlash('/foo', 'a.com')).toBe('/foo')
+ expect(addSlash('/foo', 'a.com/')).toBe('foo')
+ expect(addSlash('foo', 'a.com')).toBe('/foo')
+ expect(addSlash('foo', 'a.com/')).toBe('foo')
+ expect(addSlash('foo')).toBe('/foo')
+ expect(addSlash('/foo')).toBe('/foo')
+ })
+})
+
describe('useFetch - responseType', (): void => {
afterEach((): void => {
cleanup()
@@ -187,7 +203,7 @@ describe('useFetch - BROWSER - with ', (): void => {
fetch.mockResponseOnce(JSON.stringify(expected))
})
- it('should work correctly: useFetch({ data: [] }, [])', async (): Promise => {
+ it(`should work correctly: useFetch({ data: [] }, [])`, async (): Promise => {
const { result, waitForNextUpdate } = renderHook(
() => useFetch({ data: {} }, []), // onMount === true
{ wrapper }
@@ -232,8 +248,7 @@ describe('useFetch - BROWSER - with ', (): void => {
it('should merge the data onNewData for pagination', async (): Promise => {
const { result, waitForNextUpdate } = renderHook(
- () => useFetch({
- path: '/people',
+ () => useFetch('/people', {
data: { no: 'way' },
onNewData: (currData, newData) => ({ ...currData, ...newData })
}, []), // onMount === true
@@ -251,14 +266,13 @@ describe('useFetch - BROWSER - with ', (): void => {
it('should not make another request when there is no more data `perPage` pagination', async (): Promise => {
fetch.resetMocks()
const expected1 = [1, 2, 3]
- fetch.mockResponse(JSON.stringify(expected1))
+ fetch.mockResponseOnce(JSON.stringify(expected1))
+ .mockResponseOnce(JSON.stringify([4]))
const { result, rerender, waitForNextUpdate } = renderHook(
({ page }) => useFetch(`https://example.com?page=${page}`, {
data: [],
perPage: 3,
onNewData: (currData, newData) => {
- // to imitate getting a response with less items
- if (page === 2) return [...currData, 4]
return [...currData, ...newData]
}
}, [page]), // onMount === true
@@ -276,16 +290,15 @@ describe('useFetch - BROWSER - with ', (): void => {
expect(result.current.data).toEqual([...expected1, 4])
expect(fetch.mock.calls.length).toBe(2)
act(() => rerender({ page: 3 }))
- await waitForNextUpdate()
expect(result.current.data).toEqual([...expected1, 4])
expect(fetch.mock.calls.length).toBe(2)
})
- it('should execute GET using Provider url: useFetch({ path: "/people" }, [])', async (): Promise<
+ it(`should execute GET using Provider url: useFetch('/people', [])`, async (): Promise<
void
> => {
const { result, waitForNextUpdate } = renderHook(
- () => useFetch({ path: '/people' }, []), // onMount === true
+ () => useFetch('/people', []), // onMount === true
{ wrapper }
)
expect(result.current.loading).toBe(true)
@@ -349,13 +362,12 @@ describe('timeouts', (): void => {
const onAbort = jest.fn()
const onTimeout = jest.fn()
const { result, waitForNextUpdate } = renderHook(
- () => useFetch({
+ () => useFetch('/todos', {
retries: 1,
// TODO: this test times out if `retryDelay > 0`
// works in apps, not sure how to advance the timers correctly
retryDelay: 0,
timeout,
- path: '/todos',
onAbort,
onTimeout
}, []), // onMount === true
@@ -496,8 +508,7 @@ describe('useFetch - BROWSER - with - Managed State', (): void => {
it('should re-run the request when onUpdate dependencies are updated', async (): Promise => {
const { result, waitForNextUpdate, rerender } = renderHook(
- ({ initialValue }) => useFetch({
- path: `/${initialValue}`,
+ ({ initialValue }) => useFetch(`/${initialValue}`, {
data: {}
}, [initialValue]), // (onMount && onUpdate) === true
{
@@ -520,8 +531,7 @@ describe('useFetch - BROWSER - with - Managed State', (): void => {
it('should fetch cached data when cached path is requested', async (): Promise => {
const { result, waitForNextUpdate, rerender } = renderHook(
- ({ initialValue }) => useFetch({
- path: `/a/${initialValue}`,
+ ({ initialValue }) => useFetch(`/a/${initialValue}`, {
data: {}
}, [initialValue]), // (onMount && onUpdate) === true
{
@@ -554,21 +564,11 @@ describe('useFetch - BROWSER - interceptors', (): void => {
const snake_case = { title: 'Alex Cory', first_name: 'Alex' }
const expected = { title: 'Alex Cory', firstName: 'Alex' }
+ const request = jest.fn(({ options }) => options)
const wrapper = ({ children }: { children?: ReactNode }): ReactElement => {
- const options: Options = {
+ const options: IncomingOptions = {
interceptors: {
- request: async ({ options: opts, url, path, route }) => {
- if (path === '/path') {
- opts.data = 'path'
- }
- if (url === 'url') {
- opts.data = 'url'
- }
- if (route === '/route') {
- opts.data = 'route'
- }
- return opts
- },
+ request,
async response({ response: res }) {
if (res.data) res.data = toCamel(res.data)
return res
@@ -584,6 +584,7 @@ describe('useFetch - BROWSER - interceptors', (): void => {
afterEach((): void => {
fetch.resetMocks()
cleanup()
+ request.mockClear()
})
beforeEach((): void => {
@@ -612,11 +613,12 @@ describe('useFetch - BROWSER - interceptors', (): void => {
it('should pass the proper path string to `interceptors.request`', async (): Promise => {
const { result } = renderHook(
- () => useFetch({ path: '/path' }),
+ () => useFetch('/path'),
{ wrapper }
)
await act(result.current.get)
- expect((fetch.mock.calls[0][1] as any).data).toEqual('path')
+ expect(fetch.mock.calls[0][0]).toBe('https://example.com/path')
+ expect(request.mock.calls[0][0].path).toBe('/path')
})
it('should pass the proper route string to `interceptors.request`', async (): Promise => {
@@ -627,16 +629,18 @@ describe('useFetch - BROWSER - interceptors', (): void => {
await act(async () => {
await result.current.get('/route')
})
- expect((fetch.mock.calls[0][1] as any).data).toEqual('route')
+ expect(fetch.mock.calls[0][0]).toBe('https://example.com/route')
+ expect(request.mock.calls[0][0].route).toBe('/route')
})
it('should pass the proper url string to `interceptors.request`', async (): Promise => {
const { result } = renderHook(
- () => useFetch('url'),
+ () => useFetch(),
{ wrapper }
)
await act(result.current.get)
- expect((fetch.mock.calls[0][1] as any).data).toEqual('url')
+ expect(fetch.mock.calls[0][0]).toBe('https://example.com')
+ expect(request.mock.calls[0][0].url).toBe('https://example.com')
})
it('should still call both interceptors when using cache', async (): Promise => {
@@ -679,7 +683,7 @@ describe('useFetch - BROWSER - Overwrite Global Options set in Provider', (): vo
})
beforeEach((): void => {
- fetch.mockResponseOnce(JSON.stringify({}))
+ fetch.mockResponse(JSON.stringify({}))
})
it('should only add Content-Type: application/json for POST and PUT by default', async (): Promise => {
@@ -739,7 +743,7 @@ describe('useFetch - BROWSER - Overwrite Global Options set in Provider', (): vo
expect(fetch).toHaveBeenCalledTimes(1)
})
- it('should overwrite options set in the Provider', async (): Promise => {
+ it('should overwrite options set in the Provider and not every instance of useFetch', async (): Promise => {
const expectedHeaders = defaults.headers
const { result, waitForNextUpdate } = renderHook(
() => useFetch(globalOptions => {
@@ -755,6 +759,14 @@ describe('useFetch - BROWSER - Overwrite Global Options set in Provider', (): vo
expect(fetch.mock.calls[0][0]).toBe('https://example.com')
expect((fetch.mock.calls[0][1] as any).headers).toEqual(expectedHeaders)
expect(fetch).toHaveBeenCalledTimes(1)
+ const expectedHeadersGET = { ...defaults.headers, ...providerHeaders }
+ const { waitForNextUpdate: wait2 } = renderHook(
+ () => useFetch('/', []), // onMount === true
+ { wrapper }
+ )
+ await wait2()
+ expect((fetch.mock.calls[1][1] as any).headers).toEqual(expectedHeadersGET)
+ expect(fetch).toHaveBeenCalledTimes(2)
})
})
@@ -1060,8 +1072,7 @@ describe('useFetch - BROWSER - errors', (): void => {
fetch.resetMocks()
fetch.mockResponse('fail', { status: 401 })
const { result } = renderHook(
- () => useFetch({
- url: 'https://example.com',
+ () => useFetch('https://example.com', {
data: [],
cachePolicy: NO_CACHE
})
@@ -1094,8 +1105,7 @@ describe('useFetch - BROWSER - errors', (): void => {
fetch.resetMocks()
fetch.mockReject(expectedError)
const { result } = renderHook(
- () => useFetch({
- url: 'https://example.com/2',
+ () => useFetch('https://example.com/2', {
data: [],
cachePolicy: NO_CACHE
})
@@ -1177,7 +1187,7 @@ describe('useFetch - BROWSER - persistence', (): void => {
it('should fetch once', async (): Promise => {
const { waitForNextUpdate } = renderHook(
- () => useFetch({ url: 'https://persist.com', persist: true }, [])
+ () => useFetch('https://persist.com', { persist: true }, [])
)
await waitForNextUpdate()
expect(fetch).toHaveBeenCalledTimes(1)
@@ -1187,7 +1197,7 @@ describe('useFetch - BROWSER - persistence', (): void => {
fetch.mockResponse(JSON.stringify(unexpected))
const { result, waitForNextUpdate } = renderHook(
- () => useFetch({ url: 'https://persist.com', persist: true }, [])
+ () => useFetch('https://persist.com', { persist: true }, [])
)
await waitForNextUpdate()
expect(fetch).toHaveBeenCalledTimes(0)
@@ -1204,7 +1214,7 @@ describe('useFetch - BROWSER - persistence', (): void => {
mockdate.set('2020-01-02 02:00:00')
const { waitForNextUpdate } = renderHook(
- () => useFetch({ url: 'https://persist.com', persist: true }, [])
+ () => useFetch('https://persist.com', { persist: true }, [])
)
await waitForNextUpdate()
expect(fetch).toHaveBeenCalledTimes(1)
@@ -1212,7 +1222,7 @@ describe('useFetch - BROWSER - persistence', (): void => {
it('should have `cache` in the return of useFetch', async (): Promise => {
const { result } = renderHook(
- () => useFetch({ url: 'https://persist.com', persist: true })
+ () => useFetch('https://persist.com', { persist: true })
)
expect(result.current.cache).toBeDefined()
expect(result.current.cache.get).toBeInstanceOf(Function)
@@ -1224,14 +1234,14 @@ describe('useFetch - BROWSER - persistence', (): void => {
it('should error if passing wrong cachePolicy with persist: true', async (): Promise => {
var { result } = renderHook(
- () => useFetch({ url: 'https://persist.com', persist: true, cachePolicy: NO_CACHE }, [])
+ () => useFetch('https://persist.com', { persist: true, cachePolicy: NO_CACHE }, [])
)
expect(result.error.name).toBe('Invariant Violation')
expect(result.error.message).toBe('You cannot use option \'persist\' with cachePolicy: no-cache 🙅♂️')
// eslint-disable-next-line
var { result } = renderHook(
- () => useFetch({ url: 'https://persist.com', persist: true, cachePolicy: NETWORK_ONLY }, [])
+ () => useFetch('https://persist.com', { persist: true, cachePolicy: NETWORK_ONLY }, [])
)
expect(result.error.name).toBe('Invariant Violation')
expect(result.error.message).toBe('You cannot use option \'persist\' with cachePolicy: network-only 🙅♂️')
diff --git a/src/__tests__/useFetchArgs.test.tsx b/src/__tests__/useFetchArgs.test.tsx
index 701bc2f5..a3dc1062 100644
--- a/src/__tests__/useFetchArgs.test.tsx
+++ b/src/__tests__/useFetchArgs.test.tsx
@@ -19,26 +19,22 @@ describe('useFetchArgs: general usages', (): void => {
)
expect(result.current).toEqual({
...useFetchArgsDefaults,
+ host: 'https://example.com',
customOptions: {
...useFetchArgsDefaults.customOptions,
- url: 'https://example.com'
}
})
})
it('should create custom options with 1st arg as config object with `onMount: true`', (): void => {
const { result } = renderHook((): any =>
- useFetchArgs({
- url: 'https://example.com'
- }, []) // onMount === true
+ useFetchArgs('https://example.com', []) // onMount === true
)
expect(result.current).toEqual({
...useFetchArgsDefaults,
+ host: 'https://example.com',
customOptions: {
...useFetchArgsDefaults.customOptions,
- url: 'https://example.com'
- },
- defaults: {
loading: true,
data: undefined
},
@@ -50,9 +46,9 @@ describe('useFetchArgs: general usages', (): void => {
const { result } = renderHook((): any => useFetchArgs(), { wrapper })
expect(result.current).toStrictEqual({
...useFetchArgsDefaults,
+ host: 'https://example.com',
customOptions: {
...useFetchArgsDefaults.customOptions,
- url: 'https://example.com'
}
})
})
@@ -64,11 +60,9 @@ describe('useFetchArgs: general usages', (): void => {
)
expect(result.current).toStrictEqual({
...useFetchArgsDefaults,
+ host: 'https://cool.com',
customOptions: {
...useFetchArgsDefaults.customOptions,
- url: 'https://cool.com'
- },
- defaults: {
loading: true,
data: undefined
},
@@ -83,11 +77,9 @@ describe('useFetchArgs: general usages', (): void => {
)
expect(result.current).toStrictEqual({
...useFetchArgsDefaults,
+ host: 'https://example.com',
customOptions: {
...useFetchArgsDefaults.customOptions,
- url: 'https://example.com'
- },
- defaults: {
loading: false,
data: []
}
@@ -105,9 +97,9 @@ describe('useFetchArgs: general usages', (): void => {
const expected = {
...useFetchArgsDefaults,
+ host: 'http://localhost',
customOptions: {
...useFetchArgsDefaults.customOptions,
- url: 'http://localhost'
}
}
expect(result.current).toEqual(expected)
@@ -179,9 +171,9 @@ describe('useFetchArgs: general usages', (): void => {
const { result } = renderHook((): any => useFetchArgs(options), { wrapper })
expect(result.current).toStrictEqual({
...useFetchArgsDefaults,
+ host: 'https://example.com',
customOptions: {
...useFetchArgsDefaults.customOptions,
- url: 'https://example.com'
},
requestInit: {
...options,
@@ -198,12 +190,12 @@ describe('useFetchArgs: general usages', (): void => {
const wrapper = ({ children }: { children?: ReactNode }): ReactElement => (
{children}
)
- const { result } = renderHook((): any => useFetchArgs(), { wrapper })
+ const { result } = renderHook((): any => useFetchArgs('http://localhost'), { wrapper })
expect(result.current).toStrictEqual({
...useFetchArgsDefaults,
+ host: 'http://localhost',
customOptions: {
...useFetchArgsDefaults.customOptions,
- url: 'http://localhost'
},
requestInit: {
headers: {
@@ -234,9 +226,9 @@ describe('useFetchArgs: general usages', (): void => {
)
expect(result.current).toStrictEqual({
...useFetchArgsDefaults,
+ host: 'http://localhost',
customOptions: {
...useFetchArgsDefaults.customOptions,
- url: 'http://localhost'
},
requestInit: {
...overwriteProviderOptions,
@@ -250,35 +242,11 @@ describe('useFetchArgs: general usages', (): void => {
})
describe('useFetchArgs: Errors', (): void => {
- it('should error if no url string is set and no Provider is in place', (): void => {
- const { result } = renderHook((): any => useFetchArgs())
- expect(result.error.name).toBe('Invariant Violation')
- expect(result.error.message).toBe(
- 'The first argument of useFetch is required unless you have a global url setup like: '
- )
- })
-
- it('should error if 1st arg is object and no URL field is set in it', (): void => {
- const { result } = renderHook((): any => useFetchArgs({}))
- expect(result.error.name).toBe('Invariant Violation')
- expect(result.error.message).toBe(
- 'The first argument of useFetch is required unless you have a global url setup like: '
- )
- })
-
- it('should error if no URL is specified', (): void => {
- const { result } = renderHook((): any => useFetchArgs(''))
- expect(result.error.name).toBe('Invariant Violation')
- expect(result.error.message).toBe(
- 'The first argument of useFetch is required unless you have a global url setup like: '
- )
- })
-
it('should error if 1st and 2nd arg are both objects', (): void => {
const { result } = renderHook((): any => useFetchArgs({}, {}))
expect(result.error.name).toBe('Invariant Violation')
expect(result.error.message).toBe(
- 'You cannot have a 2nd parameter of useFetch when your first argument is an object config.'
+ 'You cannot have a 2nd parameter of useFetch as object when your first argument is an object.'
)
})
diff --git a/src/defaults.ts b/src/defaults.ts
index 9edb00cb..634d377b 100644
--- a/src/defaults.ts
+++ b/src/defaults.ts
@@ -3,6 +3,8 @@ import { isObject } from './utils'
export const useFetchArgsDefaults: UseFetchArgsReturn = {
+ host: '',
+ path: undefined,
customOptions: {
cacheLife: 0,
cachePolicy: CachePolicies.CACHE_FIRST,
@@ -11,7 +13,6 @@ export const useFetchArgsDefaults: UseFetchArgsReturn = {
onError: () => { /* do nothing */ },
onNewData: (currData: any, newData: any) => newData,
onTimeout: () => { /* do nothing */ },
- path: '',
perPage: 0,
persist: false,
responseType: ['json', 'text', 'blob', 'arrayBuffer'],
@@ -20,17 +21,15 @@ export const useFetchArgsDefaults: UseFetchArgsReturn = {
retryOn: [],
suspense: false,
timeout: 0,
- url: '',
+ // defaults
+ data: undefined,
+ loading: false
},
requestInit: {
headers: {
Accept: 'application/json, text/plain, */*'
}
},
- defaults: {
- data: undefined,
- loading: false
- },
dependencies: undefined
}
diff --git a/src/doFetchArgs.ts b/src/doFetchArgs.ts
index 487d803e..02c08bf1 100644
--- a/src/doFetchArgs.ts
+++ b/src/doFetchArgs.ts
@@ -1,16 +1,16 @@
import { HTTPMethod, Interceptors, ValueOf, DoFetchArgs, Cache } from './types'
-import { invariant, isServer, isString, isBodyObject } from './utils'
+import { invariant, isServer, isString, isBodyObject, addSlash } from './utils'
const { GET } = HTTPMethod
export default async function doFetchArgs(
initialOptions: RequestInit,
- initialURL: string,
- path: string,
method: HTTPMethod,
controller: AbortController,
cacheLife: number,
cache: Cache,
+ host?: string,
+ path?: string,
routeOrBody?: string | BodyInit | object,
bodyAs2ndParam?: BodyInit | object,
requestInterceptor?: ValueOf>
@@ -34,7 +34,7 @@ export default async function doFetchArgs(
return ''
})()
- const url = `${initialURL}${path}${route}`
+ const url = `${host}${addSlash(path, host)}${addSlash(route)}`
const body = ((): BodyInit | null => {
// FormData instanceof check should go first, because React Native's FormData implementation
@@ -68,7 +68,7 @@ export default async function doFetchArgs(
})()
const options = await (async (): Promise => {
- const opts = {
+ const opts: RequestInit = {
...initialOptions,
method,
signal: controller.signal
@@ -83,7 +83,7 @@ export default async function doFetchArgs(
if (body !== null) opts.body = body
if (requestInterceptor) {
- const interceptor = await requestInterceptor({ options: opts, url: initialURL, path, route })
+ const interceptor = await requestInterceptor({ options: opts, url: host, path, route })
return interceptor as any
}
return opts
diff --git a/src/types.ts b/src/types.ts
index db3ff2f0..12248662 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -75,13 +75,13 @@ export interface DoFetchArgs {
export interface FetchContextTypes {
url: string
- options: Options
+ options: IncomingOptions
graphql?: boolean
}
export interface FetchProviderProps {
url?: string
- options?: Options
+ options?: IncomingOptions
graphql?: boolean
children: ReactNode
}
@@ -141,7 +141,7 @@ export interface Res extends Response {
export type Req = ReqMethods & ReqBase
-export type UseFetchArgs = [(string | OptionsMaybeURL | OverwriteGlobalOptions)?, (NoUrlOptions | OverwriteGlobalOptions | any[])?, any[]?]
+export type UseFetchArgs = [(string | IncomingOptions | OverwriteGlobalOptions)?, (IncomingOptions | OverwriteGlobalOptions | any[])?, any[]?]
export type UseFetchArrayReturn = [
Req,
@@ -160,7 +160,7 @@ export type UseFetch = UseFetchArrayReturn &
UseFetchObjectReturn
export type Interceptors = {
- request?: ({ options, url, path, route }: { options: Options, url: string, path: string, route: string }) => Promise | Options
+ request?: ({ options, url, path, route }: { options: RequestInit, url?: string, path?: string, route?: string }) => Promise | RequestInit
response?: ({ response }: { response: Res }) => Promise>
}
@@ -174,36 +174,33 @@ export type Cache = {
}
export interface CustomOptions {
- cacheLife?: number
- cachePolicy?: CachePolicies
- data?: any
- interceptors?: Interceptors
- loading?: boolean
- onAbort?: () => void
- onError?: OnError
- onNewData?: (currData: any, newData: any) => any
- onTimeout?: () => void
- path?: string
- persist?: boolean
- perPage?: number
- responseType?: ResponseType
- retries?: number
- retryOn?: RetryOn
- retryDelay?: RetryDelay
- suspense?: boolean
- timeout?: number
- url?: string
+ cacheLife: number
+ cachePolicy: CachePolicies
+ data: any
+ interceptors: Interceptors
+ loading: boolean
+ onAbort: () => void
+ onError: OnError
+ onNewData: (currData: any, newData: any) => any
+ onTimeout: () => void
+ persist: boolean
+ perPage: number
+ responseType: ResponseType
+ retries: number
+ retryOn: RetryOn
+ retryDelay: RetryDelay
+ suspense: boolean
+ timeout: number
}
+// these are the possible options that can be passed
+export type IncomingOptions = Partial &
+ Omit & { body?: BodyInit | object | null }
+// these options have `context` and `defaults` applied so
+// the values should all be filled
export type Options = CustomOptions &
Omit & { body?: BodyInit | object | null }
-export type NoUrlOptions = Omit
-
-export type OptionsMaybeURL = NoUrlOptions &
- Partial> & { url?: string }
-
-// TODO: this is still yet to be implemented
export type OverwriteGlobalOptions = (options: Options) => Options
export type RetryOn = (({ attempt, error, response }: RetryOpts) => Promise) | number[]
@@ -215,6 +212,8 @@ export type ResponseType = BodyInterfaceMethods | BodyInterfaceMethods[]
export type OnError = ({ error }: { error: Error }) => void
export type UseFetchArgsReturn = {
+ host: string
+ path?: string
customOptions: {
cacheLife: number
cachePolicy: CachePolicies
@@ -223,7 +222,6 @@ export type UseFetchArgsReturn = {
onError: OnError
onNewData: (currData: any, newData: any) => any
onTimeout: () => void
- path: string
perPage: number
persist: boolean
responseType: ResponseType
@@ -232,13 +230,11 @@ export type UseFetchArgsReturn = {
retryOn: RetryOn | undefined
suspense: boolean
timeout: number
- url: string
- }
- requestInit: RequestInit
- defaults: {
+ // defaults
loading: boolean
data?: any
}
+ requestInit: RequestInit
dependencies?: any[]
}
diff --git a/src/useFetch.ts b/src/useFetch.ts
index ff85361f..10157d45 100644
--- a/src/useFetch.ts
+++ b/src/useFetch.ts
@@ -26,7 +26,7 @@ const { CACHE_FIRST } = CachePolicies
function useFetch(...args: UseFetchArgs): UseFetch {
- const { customOptions, requestInit, defaults, dependencies } = useFetchArgs(...args)
+ const { host, path, customOptions, requestInit, dependencies } = useFetchArgs(...args)
const {
cacheLife,
cachePolicy, // 'cache-first' by default
@@ -35,7 +35,6 @@ function useFetch(...args: UseFetchArgs): UseFetch {
onError,
onNewData,
onTimeout,
- path,
perPage,
persist,
responseType,
@@ -44,7 +43,7 @@ function useFetch(...args: UseFetchArgs): UseFetch {
retryOn,
suspense,
timeout,
- url: initialURL,
+ ...defaults
} = customOptions
const cache = useCache({ persist, cacheLife, cachePolicy })
@@ -75,12 +74,12 @@ function useFetch(...args: UseFetchArgs): UseFetch {
const { url, options, response } = await doFetchArgs(
requestInit,
- initialURL,
- path,
method,
theController,
cacheLife,
cache,
+ host,
+ path,
routeOrBody,
body,
interceptors.request
@@ -88,27 +87,11 @@ function useFetch(...args: UseFetchArgs): UseFetch {
error.current = undefined
- if (response.isCached && cachePolicy === CACHE_FIRST) {
- try {
- res.current = response.cached as Res
- const theData = await tryGetData(response.cached, defaults.data, responseType)
- res.current.data = theData
- res.current = interceptors.response ? await interceptors.response({ response: res.current }) : res.current
- invariant('data' in res.current, 'You must have `data` field on the Response returned from your `interceptors.response`')
- data.current = res.current.data as TData
- if (!suspense && mounted.current) forceUpdate()
- return data.current
- } catch (err) {
- error.current = err
- if (mounted.current) forceUpdate()
- }
- }
-
- if (!suspense) setLoading(true)
-
// don't perform the request if there is no more data to fetch (pagination)
if (perPage > 0 && !hasMore.current && !error.current) return data.current
+ if (!suspense) setLoading(true)
+
const timer = timeout && setTimeout(() => {
timedout.current = true
theController.abort()
@@ -119,7 +102,11 @@ function useFetch(...args: UseFetchArgs): UseFetch {
let newRes
try {
- newRes = await fetch(url, options)
+ if (response.isCached && cachePolicy === CACHE_FIRST) {
+ newRes = response.cached as Response
+ } else {
+ newRes = await fetch(url, options)
+ }
res.current = newRes.clone()
newData = await tryGetData(newRes, defaults.data, responseType)
@@ -209,7 +196,7 @@ function useFetch(...args: UseFetchArgs): UseFetch {
}
return doFetch
- }, [isServer, onAbort, requestInit, initialURL, path, interceptors, cachePolicy, perPage, timeout, persist, cacheLife, onTimeout, defaults.data, onNewData, forceUpdate, suspense])
+ }, [isServer, onAbort, requestInit, host, path, interceptors, cachePolicy, perPage, timeout, persist, cacheLife, onTimeout, defaults.data, onNewData, forceUpdate, suspense])
const post = useCallback(makeFetch(HTTPMethod.POST), [makeFetch])
const del = useCallback(makeFetch(HTTPMethod.DELETE), [makeFetch])
diff --git a/src/useFetchArgs.ts b/src/useFetchArgs.ts
index 6200ff51..23c9fa70 100644
--- a/src/useFetchArgs.ts
+++ b/src/useFetchArgs.ts
@@ -1,155 +1,97 @@
-import { OptionsMaybeURL, NoUrlOptions, CachePolicies, Interceptors, OverwriteGlobalOptions, Options, RetryOn, RetryDelay, UseFetchArgsReturn, ResponseType, OnError } from './types'
+import { Interceptors, OverwriteGlobalOptions, Options, IncomingOptions, UseFetchArgsReturn, CustomOptions } from './types'
import { isString, isObject, invariant, pullOutRequestInit, isFunction, isPositiveNumber } from './utils'
import { useContext, useMemo } from 'react'
import FetchContext from './FetchContext'
-import defaults from './defaults'
+import defaults, { useFetchArgsDefaults } from './defaults'
-const useField = (
- field: keyof OptionsMaybeURL | keyof NoUrlOptions,
- urlOrOptions?: string | OptionsMaybeURL,
- optionsNoURLs?: NoUrlOptions | any[]
-) => {
- const context = useContext(FetchContext)
- const contextOptions = context.options || {}
- return useMemo((): DV => {
- if (isObject(urlOrOptions) && field in urlOrOptions) return urlOrOptions[field]
- if (isObject(optionsNoURLs) && field in optionsNoURLs) {
- return (optionsNoURLs as NoUrlOptions)[field as keyof NoUrlOptions]
- }
- if (field in contextOptions) return contextOptions[field]
- return defaults[field]
- }, [urlOrOptions, field, optionsNoURLs, contextOptions])
-}
-
export default function useFetchArgs(
- urlOrOptionsOrOverwriteGlobal?: string | OptionsMaybeURL | OverwriteGlobalOptions,
- optionsNoURLsOrOverwriteGlobalOrDeps?: NoUrlOptions | OverwriteGlobalOptions | any[],
+ urlOrPathOrOptionsOrOverwriteGlobalOptions?: string | IncomingOptions | OverwriteGlobalOptions,
+ optionsOrOverwriteGlobalOrDeps?: IncomingOptions | OverwriteGlobalOptions | any[],
deps?: any[]
): UseFetchArgsReturn {
- const context = useContext(FetchContext)
- context.options = useMemo(() => {
- const overwriteGlobalOptions = (isFunction(urlOrOptionsOrOverwriteGlobal) ? urlOrOptionsOrOverwriteGlobal : isFunction(optionsNoURLsOrOverwriteGlobalOrDeps) && optionsNoURLsOrOverwriteGlobalOrDeps) as OverwriteGlobalOptions
- if (!overwriteGlobalOptions) return context.options
- // make a copy so we make sure not to modify the original context
- return overwriteGlobalOptions({ ...context.options } as Options)
- }, [context.options, optionsNoURLsOrOverwriteGlobalOrDeps, urlOrOptionsOrOverwriteGlobal])
-
- const urlOrOptions = urlOrOptionsOrOverwriteGlobal as string | OptionsMaybeURL
- const optionsNoURLs = optionsNoURLsOrOverwriteGlobalOrDeps as NoUrlOptions
-
invariant(
- !(isObject(urlOrOptions) && isObject(optionsNoURLs)),
- 'You cannot have a 2nd parameter of useFetch when your first argument is an object config.'
+ !(isObject(urlOrPathOrOptionsOrOverwriteGlobalOptions) && isObject(optionsOrOverwriteGlobalOrDeps)),
+ 'You cannot have a 2nd parameter of useFetch as object when your first argument is an object.'
)
+ const context = useContext(FetchContext)
- const url = useMemo((): string => {
- if (isString(urlOrOptions) && urlOrOptions) return urlOrOptions as string
- if (isObject(urlOrOptions) && !!urlOrOptions.url) return urlOrOptions.url
+ const host = useMemo((): string => {
+ const maybeHost = urlOrPathOrOptionsOrOverwriteGlobalOptions as string
+ if (isString(maybeHost) && maybeHost.includes('://')) return maybeHost
if (context.url) return context.url
- return defaults.url
- }, [context.url, urlOrOptions])
+ return defaults.host
+ }, [context.url, urlOrPathOrOptionsOrOverwriteGlobalOptions])
+
+ const path = useMemo((): string | undefined => {
+ const maybePath = urlOrPathOrOptionsOrOverwriteGlobalOptions as string
+ if (isString(maybePath) && !maybePath.includes('://')) return maybePath
+ }, [urlOrPathOrOptionsOrOverwriteGlobalOptions])
+
+ const overwriteGlobalOptions = useMemo((): OverwriteGlobalOptions | undefined => {
+ if (isFunction(urlOrPathOrOptionsOrOverwriteGlobalOptions)) return urlOrPathOrOptionsOrOverwriteGlobalOptions as OverwriteGlobalOptions
+ if (isFunction(optionsOrOverwriteGlobalOrDeps)) return optionsOrOverwriteGlobalOrDeps as OverwriteGlobalOptions
+ }, [])
+
+ const options = useMemo(() => {
+ let localOptions = { headers: {} } as IncomingOptions
+ if (isObject(urlOrPathOrOptionsOrOverwriteGlobalOptions)) {
+ localOptions = urlOrPathOrOptionsOrOverwriteGlobalOptions as IncomingOptions
+ } else if (isObject(optionsOrOverwriteGlobalOrDeps)) {
+ localOptions = optionsOrOverwriteGlobalOrDeps as IncomingOptions
+ }
+ let globalOptions = context.options
+ const finalOptions = {
+ ...defaults,
+ ...globalOptions,
+ ...localOptions,
+ headers: {
+ ...defaults.headers,
+ ...globalOptions.headers,
+ ...localOptions.headers
+ } as Headers
+ } as Options
+ if (overwriteGlobalOptions) return overwriteGlobalOptions(finalOptions)
+ return finalOptions
+ }, [urlOrPathOrOptionsOrOverwriteGlobalOptions, overwriteGlobalOptions, context.options])
- invariant(
- !!url,
- 'The first argument of useFetch is required unless you have a global url setup like: '
- )
+ const requestInit = useMemo(() => pullOutRequestInit(options), [options])
const dependencies = useMemo((): any[] | undefined => {
- if (Array.isArray(optionsNoURLsOrOverwriteGlobalOrDeps)) return optionsNoURLsOrOverwriteGlobalOrDeps
+ if (Array.isArray(optionsOrOverwriteGlobalOrDeps)) return optionsOrOverwriteGlobalOrDeps
if (Array.isArray(deps)) return deps
return defaults.dependencies
- }, [optionsNoURLsOrOverwriteGlobalOrDeps, deps])
+ }, [optionsOrOverwriteGlobalOrDeps, deps])
- const data = useField('data', urlOrOptions, optionsNoURLs)
- const cacheLife = useField('cacheLife', urlOrOptions, optionsNoURLs)
+ const { cacheLife, retries, retryDelay, retryOn } = options
invariant(Number.isInteger(cacheLife) && cacheLife >= 0, '`cacheLife` must be a number >= 0')
- const cachePolicy = useField('cachePolicy', urlOrOptions, optionsNoURLs)
- const onAbort = useField<() => void>('onAbort', urlOrOptions, optionsNoURLs)
- const onError = useField('onError', urlOrOptions, optionsNoURLs)
- const onNewData = useField<() => void>('onNewData', urlOrOptions, optionsNoURLs)
- const onTimeout = useField<() => void>('onTimeout', urlOrOptions, optionsNoURLs)
- const path = useField('path', urlOrOptions, optionsNoURLs)
- const perPage = useField('perPage', urlOrOptions, optionsNoURLs)
- const persist = useField('persist', urlOrOptions, optionsNoURLs)
- const responseType = useField('responseType', urlOrOptions, optionsNoURLs)
- const retries = useField('retries', urlOrOptions, optionsNoURLs)
invariant(Number.isInteger(retries) && retries >= 0, '`retries` must be a number >= 0')
- const retryDelay = useField('retryDelay', urlOrOptions, optionsNoURLs)
invariant(isFunction(retryDelay) || Number.isInteger(retryDelay as number) && retryDelay >= 0, '`retryDelay` must be a positive number or a function returning a positive number.')
- const retryOn = useField('retryOn', urlOrOptions, optionsNoURLs)
const isValidRetryOn = isFunction(retryOn) || (Array.isArray(retryOn) && retryOn.every(isPositiveNumber))
invariant(isValidRetryOn, '`retryOn` must be an array of positive numbers or a function returning a boolean.')
- const suspense = useField('suspense', urlOrOptions, optionsNoURLs)
- const timeout = useField('timeout', urlOrOptions, optionsNoURLs)
-
- const loading = useMemo((): boolean => {
- if (isObject(urlOrOptions)) return !!urlOrOptions.loading || Array.isArray(dependencies)
- if (isObject(optionsNoURLs)) return !!optionsNoURLs.loading || Array.isArray(dependencies)
- return defaults.loading || Array.isArray(dependencies)
- }, [urlOrOptions, dependencies, optionsNoURLs])
+ const loading = options.loading || Array.isArray(dependencies)
const interceptors = useMemo((): Interceptors => {
- const contextInterceptors = context.options && (context.options.interceptors || {})
- const final: Interceptors = { ...contextInterceptors }
- if (isObject(urlOrOptions) && isObject(urlOrOptions.interceptors)) {
- if (urlOrOptions.interceptors.request) final.request = urlOrOptions.interceptors.request
- if (urlOrOptions.interceptors.response) final.response = urlOrOptions.interceptors.response
- }
- if (isObject(optionsNoURLs) && isObject(optionsNoURLs.interceptors)) {
- if (optionsNoURLs.interceptors.request) final.request = optionsNoURLs.interceptors.request
- if (optionsNoURLs.interceptors.response) final.response = optionsNoURLs.interceptors.response
- }
+ const final: Interceptors = {}
+ if ('request' in options.interceptors) final.request = options.interceptors.request
+ if ('response' in options.interceptors) final.response = options.interceptors.response
return final
- }, [context.options, urlOrOptions, optionsNoURLs])
-
- const requestInit = useMemo((): RequestInit => {
- const contextRequestInit = pullOutRequestInit(context.options as OptionsMaybeURL)
+ }, [options])
- const requestInitOptions = isObject(urlOrOptions)
- ? urlOrOptions
- : isObject(optionsNoURLs)
- ? optionsNoURLs
- : {}
-
- const requestInit = pullOutRequestInit(requestInitOptions)
-
- return {
- ...contextRequestInit,
- ...requestInit,
- headers: {
- ...defaults.headers,
- ...contextRequestInit.headers,
- ...requestInit.headers
- }
- }
- }, [context.options, urlOrOptions, optionsNoURLs])
+ const customOptions = useMemo((): CustomOptions => {
+ const customOptionKeys = Object.keys(useFetchArgsDefaults.customOptions) as (keyof CustomOptions)[] // Array
+ const customOptions = customOptionKeys.reduce((opts, key) => {
+ (opts as any)[key] = options[key]
+ return opts
+ }, {} as CustomOptions)
+ return { ...customOptions, interceptors, loading }
+ }, [interceptors, loading])
return {
- customOptions: {
- cacheLife,
- cachePolicy,
- interceptors,
- onAbort,
- onError,
- onNewData,
- onTimeout,
- path,
- persist,
- perPage,
- responseType,
- retries,
- retryDelay,
- retryOn,
- suspense,
- timeout,
- url,
- },
+ host,
+ path,
+ customOptions,
requestInit,
- defaults: {
- data,
- loading
- },
dependencies
}
}
diff --git a/src/utils.ts b/src/utils.ts
index 6e1c456e..b9c45511 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,6 +1,6 @@
import { useMemo, useEffect, MutableRefObject, useRef, useCallback, DependencyList } from 'react'
import useSSR from 'use-ssr'
-import { RequestInitJSON, OptionsMaybeURL, Res, HTTPMethod, ResponseType } from './types'
+import { RequestInitJSON, Options, Res, HTTPMethod, ResponseType } from './types'
import { FunctionKeys, NonFunctionKeys } from 'utility-types'
/**
@@ -94,7 +94,7 @@ export const isNumber = (v: any): boolean => Object.prototype.toString.call(v) =
* Makes an object that will match the standards of a normal fetch's options
* aka: pulls out all useFetch's special options like "onMount"
*/
-export const pullOutRequestInit = (options?: OptionsMaybeURL): RequestInit => {
+export const pullOutRequestInit = (options?: Options): RequestInit => {
if (!options) return {}
const requestInitFields = [
'body',
@@ -153,12 +153,12 @@ export const tryGetData = async (res: Response | undefined, defaultData: any, re
return !isEmpty(defaultData) && isEmpty(data) ? defaultData : data
}
-const tryRetry = async (res: Response, types: ResponseType): Promise => {
+const tryRetry = async (res: Response, types: ResponseType, i: number = 0): Promise => {
try {
- return (res.clone() as any)[types[0]]()
+ return await (res.clone() as any)[types[i]]()
} catch (error) {
- if (types.length === 1) throw error
- return tryRetry(res.clone(), (types as any).slice(1))
+ if (types.length - 1 === i) throw error
+ return tryRetry(res.clone(), types, ++i)
}
}
@@ -247,3 +247,29 @@ export const makeError = (name: string | number, message: string) => {
error.name = name + ''
return error
}
+
+/**
+ * Determines if we need to add a slash to front
+ * of a path, and adds it if we do.
+ * Cases:
+ * (path = '', url = '' || null | undefined) => ''
+ * (path = '?foo=bar', url = 'a.com') => '?foo=bar'
+ * (path = '?foo=bar', url = 'a.com/') => '?foo=bar'
+ * (path = 'foo', url = 'a.com') => '/foo'
+ * (path = 'foo', url = 'a.com/') => 'foo'
+ * (path = '/foo', url = 'a.com') => '/foo'
+ * (path = '/foo', url = 'a.com/') => 'foo'
+ * (path = '?foo=bar') => '?foo=bar'
+ * (path = 'foo') => '/foo'
+ * (path = '/foo') => '/foo'
+ */
+export const addSlash = (input?: string, url?: string) => {
+ if (!input) return ''
+ if (!url) {
+ if (input.startsWith('?') || input.startsWith('/')) return input
+ return `/${input}`
+ }
+ if (url.endsWith('/') && input.startsWith('/')) return input.substr(1)
+ if (!url.endsWith('/') && !input.startsWith('/') && !input.startsWith('?')) return `/${input}`
+ return input
+}