diff --git a/README.md b/README.md
index c4b70e0c..924b6acc 100644
--- a/README.md
+++ b/README.md
@@ -964,6 +964,7 @@ Todos
- .json() ['application/json']
- .text() ['text/plain']
- .blob() ['image/png', 'application/octet-stream']
+- [ ] is making a [gitpod](https://www.gitpod.io/docs/configuration/) useful here? 🤔
- [ ] suspense
- [ ] triggering it from outside the `` component.
- add `.read()` to `request`
@@ -997,21 +998,6 @@ Todos
- [ ] show comparison with Apollo
- [ ] figure out a good way to show side-by-side comparisons
- [ ] show comparison with Axios
-- [ ] maybe add syntax for middle helpers for inline `headers` or `queries` like this:
-
- ```jsx
- const request = useFetch('https://example.com')
-
- request
- .headers({
- auth: jwt // this would inline add the `auth` header
- })
- .query({ // might have to use .params({ }) since we're using .query() for GraphQL
- no: 'way' // this would inline make the url: https://example.com?no=way
- })
- .get()
- ```
-
- [ ] potential option ideas
```jsx
@@ -1021,6 +1007,14 @@ Todos
// to overwrite those of `useFetch` for
// `useMutation` and `useQuery`
},
+ responseType: 'json', // similar to axios
+ // OR can be an array. We will try to get the `data`
+ // by attempting to extract it via these body interface
+ // methods, one by one in this order
+ responseType: ['json', 'text', 'blob', 'formData', 'arrayBuffer'],
+ // ALSO, maybe there's a way to guess the proper `body interface method` for the correct response content-type.
+ // here's a stackoverflow with someone who's tried: https://bit.ly/2X8iaVG
+
// Allows you to pass in your own cache to useFetch
// This is controversial though because `cache` is an option in the requestInit
// and it's value is a string. See: https://developer.mozilla.org/en-US/docs/Web/API/Request/cache
diff --git a/package.json b/package.json
index 696aec34..7dd8dc20 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "use-http",
- "version": "0.4.4",
+ "version": "0.4.5",
"homepage": "http://use-http.com",
"main": "dist/index.js",
"license": "MIT",
diff --git a/src/__tests__/doFetchArgs.test.tsx b/src/__tests__/doFetchArgs.test.tsx
index e819b01c..01b356bb 100644
--- a/src/__tests__/doFetchArgs.test.tsx
+++ b/src/__tests__/doFetchArgs.test.tsx
@@ -1,6 +1,6 @@
import doFetchArgs from '../doFetchArgs'
import { HTTPMethod } from '../types'
-import { defaults } from '../useFetchArgs'
+import defaults from '../defaults'
import useCache from '../useCache'
describe('doFetchArgs: general usages', (): void => {
diff --git a/src/__tests__/useFetch.test.tsx b/src/__tests__/useFetch.test.tsx
index fb47a70d..867149d5 100644
--- a/src/__tests__/useFetch.test.tsx
+++ b/src/__tests__/useFetch.test.tsx
@@ -1,14 +1,16 @@
/* eslint-disable no-var */
/* eslint-disable camelcase */
/* eslint-disable @typescript-eslint/camelcase */
-import React, { ReactElement, ReactNode } from 'react'
+import React, { ReactElement, ReactNode, useEffect } from 'react'
import { useFetch, Provider } from '..'
import { cleanup } from '@testing-library/react'
+import * as test from '@testing-library/react'
import { FetchMock } from 'jest-fetch-mock'
import { toCamel } from 'convert-keys'
import { renderHook, act } from '@testing-library/react-hooks'
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'
@@ -92,11 +94,42 @@ describe('useFetch - BROWSER - basic functionality', (): void => {
var formData = new FormData()
formData.append('username', 'AlexCory')
await result.current.post(formData)
- const options = fetch.mock.calls[0][1] || {}
+ const options = fetch.mock.calls[0][1] || { headers: {} }
expect(options.method).toBe('POST')
- expect(options.headers).toBeUndefined()
+ expect('Content-Type' in (options as any).headers).toBe(false)
})
})
+
+ it('should not cause infinite loop with `[request]` as dependency', async () => {
+ function Section() {
+ const { request, data } = useFetch('https://a.co')
+ useEffect(() => {
+ request.get()
+ }, [request])
+ return
{JSON.stringify(data)}
+ }
+ const { container } = test.render()
+
+ await test.act(async (): Promise => await sleep(100))
+ expect(JSON.parse(container.textContent as string)).toEqual(expected)
+ })
+
+ it('should not cause infinite loop with `[response]` as dependency', async () => {
+ function Section() {
+ const { request, response, data } = useFetch('https://a.co')
+ useEffect(() => {
+ (async () => {
+ await request.get()
+ if (!response.ok) console.error('no okay')
+ })()
+ }, [request, response])
+ return {JSON.stringify(data)}
+ }
+ const { container } = test.render()
+
+ await test.act(async (): Promise => await sleep(100))
+ expect(JSON.parse(container.textContent as string)).toEqual(expected)
+ })
})
describe('useFetch - BROWSER - with ', (): void => {
@@ -591,8 +624,9 @@ describe('useFetch - BROWSER - Overwrite Global Options set in Provider', (): vo
})
it('should only add Content-Type: application/json for POST and PUT by default', async (): Promise => {
- const expectedHeadersGET = providerHeaders
+ const expectedHeadersGET = { ...defaults.headers, ...providerHeaders }
const expectedHeadersPOSTandPUT = {
+ ...defaults.headers,
...providerHeaders,
'Content-Type': 'application/json'
}
@@ -613,7 +647,10 @@ describe('useFetch - BROWSER - Overwrite Global Options set in Provider', (): vo
})
it('should have the correct headers set in the options set in the Provider', async (): Promise => {
- const expectedHeaders = providerHeaders
+ const expectedHeaders = {
+ ...defaults.headers,
+ ...providerHeaders
+ }
const { result } = renderHook(
() => useFetch(),
{ wrapper }
@@ -625,7 +662,7 @@ describe('useFetch - BROWSER - Overwrite Global Options set in Provider', (): vo
})
it('should overwrite url and options set in the Provider', async (): Promise => {
- const expectedHeaders = undefined
+ const expectedHeaders = defaults.headers
const expectedURL = 'https://example2.com'
const { result, waitForNextUpdate } = renderHook(
() => useFetch(expectedURL, globalOptions => {
@@ -644,7 +681,7 @@ describe('useFetch - BROWSER - Overwrite Global Options set in Provider', (): vo
})
it('should overwrite options set in the Provider', async (): Promise => {
- const expectedHeaders = undefined
+ const expectedHeaders = defaults.headers
const { result, waitForNextUpdate } = renderHook(
() => useFetch(globalOptions => {
// TODO: fix the generics here so it knows when a header
diff --git a/src/__tests__/useFetchArgs.test.tsx b/src/__tests__/useFetchArgs.test.tsx
index 790cde07..701bc2f5 100644
--- a/src/__tests__/useFetchArgs.test.tsx
+++ b/src/__tests__/useFetchArgs.test.tsx
@@ -1,5 +1,6 @@
import { renderHook } from '@testing-library/react-hooks'
-import useFetchArgs, { useFetchArgsDefaults } from '../useFetchArgs'
+import useFetchArgs from '../useFetchArgs'
+import defaults, { useFetchArgsDefaults } from '../defaults'
import React, { ReactElement, ReactNode } from 'react'
import { Provider } from '..'
@@ -183,7 +184,11 @@ describe('useFetchArgs: general usages', (): void => {
url: 'https://example.com'
},
requestInit: {
- ...options
+ ...options,
+ headers: {
+ ...defaults.headers,
+ ...options.headers
+ }
}
})
})
@@ -201,7 +206,10 @@ describe('useFetchArgs: general usages', (): void => {
url: 'http://localhost'
},
requestInit: {
- ...options
+ headers: {
+ ...defaults.headers,
+ ...options.headers
+ }
}
})
})
@@ -231,7 +239,11 @@ describe('useFetchArgs: general usages', (): void => {
url: 'http://localhost'
},
requestInit: {
- ...overwriteProviderOptions
+ ...overwriteProviderOptions,
+ headers: {
+ ...defaults.headers,
+ ...overwriteProviderOptions.headers
+ }
}
})
})
diff --git a/src/defaults.ts b/src/defaults.ts
new file mode 100644
index 00000000..f06f7052
--- /dev/null
+++ b/src/defaults.ts
@@ -0,0 +1,38 @@
+import { Flatten, CachePolicies, UseFetchArgsReturn } from './types'
+import { isObject } from './utils'
+
+
+export const useFetchArgsDefaults: UseFetchArgsReturn = {
+ customOptions: {
+ cacheLife: 0,
+ cachePolicy: CachePolicies.CACHE_FIRST,
+ interceptors: {},
+ onAbort: () => { /* do nothing */ },
+ onNewData: (currData: any, newData: any) => newData,
+ onTimeout: () => { /* do nothing */ },
+ path: '',
+ perPage: 0,
+ persist: false,
+ retries: 0,
+ retryDelay: 1000,
+ retryOn: [],
+ suspense: false,
+ timeout: 0,
+ url: '',
+ },
+ requestInit: {
+ headers: {
+ Accept: 'application/json, text/plain, */*'
+ }
+ },
+ defaults: {
+ data: undefined,
+ loading: false
+ },
+ dependencies: undefined
+}
+
+export default Object.entries(useFetchArgsDefaults).reduce((acc, [key, value]) => {
+ if (isObject(value)) return { ...acc, ...value }
+ return { ...acc, [key]: value }
+}, {} as Flatten)
diff --git a/src/doFetchArgs.ts b/src/doFetchArgs.ts
index 0999dcce..3489b434 100644
--- a/src/doFetchArgs.ts
+++ b/src/doFetchArgs.ts
@@ -44,8 +44,8 @@ export default async function doFetchArgs(
((bodyAs2ndParam as any) instanceof FormData ||
(bodyAs2ndParam as any) instanceof URLSearchParams)
) return bodyAs2ndParam as any
- if (isBodyObject(bodyAs2ndParam)) return JSON.stringify(bodyAs2ndParam)
- if (isBodyObject(initialOptions.body)) return JSON.stringify(initialOptions.body)
+ if (isBodyObject(bodyAs2ndParam) || isString(bodyAs2ndParam)) return JSON.stringify(bodyAs2ndParam)
+ if (isBodyObject(initialOptions.body) || isString(bodyAs2ndParam)) return JSON.stringify(initialOptions.body)
return null
})()
diff --git a/src/types.ts b/src/types.ts
index e96d7340..4d44dcd6 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -206,6 +206,32 @@ export type OverwriteGlobalOptions = (options: Options) => Options
export type RetryOn = (({ attempt, error, response }: { attempt: number, error: Error, response: Res | null }) => boolean) | number[]
export type RetryDelay = (({ attempt, error, response }: { attempt: number, error: Error, response: Res | null }) => number) | number
+export type UseFetchArgsReturn = {
+ customOptions: {
+ cacheLife: number
+ cachePolicy: CachePolicies
+ interceptors: Interceptors
+ onAbort: () => void
+ onNewData: (currData: any, newData: any) => any
+ onTimeout: () => void
+ path: string
+ perPage: number
+ persist: boolean
+ retries: number
+ retryDelay: RetryDelay
+ retryOn: RetryOn | undefined
+ suspense: boolean
+ timeout: number
+ url: string
+ }
+ requestInit: RequestInit
+ defaults: {
+ loading: boolean
+ data?: any
+ }
+ dependencies?: any[]
+}
+
/**
* Helpers
*/
diff --git a/src/useFetch.ts b/src/useFetch.ts
index d3a237ef..9a9bccbc 100644
--- a/src/useFetch.ts
+++ b/src/useFetch.ts
@@ -59,15 +59,18 @@ function useFetch(...args: UseFetchArgs): UseFetch {
const suspender = useRef>()
const mounted = useRef(false)
- const [loading, setLoading] = useState(defaults.loading)
+ const loading = useRef(defaults.loading)
+ const setLoadingState = useState(defaults.loading)[1]
+ const setLoading = (v: boolean) => {
+ if (!mounted.current) return
+ loading.current = v
+ setLoadingState(v)
+ }
const forceUpdate = useReducer(() => ({}), [])[1]
const makeFetch = useDeepCallback((method: HTTPMethod): FetchData => {
- const doFetch = async (
- routeOrBody?: RouteOrBody,
- body?: Body
- ): Promise => {
+ const doFetch = async (routeOrBody?: RouteOrBody, body?: Body): Promise => {
if (isServer) return // for now, we don't do anything on the server
controller.current = new AbortController()
controller.current.signal.onabort = onAbort
@@ -91,8 +94,9 @@ function useFetch(...args: UseFetchArgs): UseFetch {
if (response.isCached && cachePolicy === CACHE_FIRST) {
try {
res.current = response.cached as Res
- res.current.data = await tryGetData(response.cached, defaults.data)
- data.current = res.current.data as TData
+ const d = await tryGetData(response.cached, defaults.data)
+ res.current.data = d
+ data.current = d
if (!suspense && mounted.current) forceUpdate()
return data.current
} catch (err) {
@@ -101,7 +105,7 @@ function useFetch(...args: UseFetchArgs): UseFetch {
}
}
- if (!suspense && mounted.current) setLoading(true)
+ 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
@@ -137,8 +141,8 @@ function useFetch(...args: UseFetchArgs): UseFetch {
) && retries > 0 && retries > attempt.current
if (shouldRetry) {
- const data = await retry(opts, routeOrBody, body)
- return data
+ const theData = await retry(opts, routeOrBody, body)
+ return theData
}
if (cachePolicy === CACHE_FIRST) {
@@ -158,8 +162,8 @@ function useFetch(...args: UseFetchArgs): UseFetch {
) && retries > 0 && retries > attempt.current
if (shouldRetry) {
- const temp = await retry(opts, routeOrBody, body)
- return temp
+ const theData = await retry(opts, routeOrBody, body)
+ return theData
}
if (err.name !== 'AbortError') error.current = makeError(err.name, err.message)
@@ -170,7 +174,7 @@ function useFetch(...args: UseFetchArgs): UseFetch {
}
if (newRes && !newRes.ok && !error.current) error.current = makeError(newRes.status, newRes.statusText)
- if (!suspense && mounted.current) setLoading(false)
+ if (!suspense) setLoading(false)
if (attempt.current === retries) attempt.current = 0
return data.current
@@ -210,21 +214,22 @@ function useFetch(...args: UseFetchArgs): UseFetch {
const post = useCallback(makeFetch(HTTPMethod.POST), [makeFetch])
const del = useCallback(makeFetch(HTTPMethod.DELETE), [makeFetch])
- const request: Req = {
- get: useCallback(makeFetch(HTTPMethod.GET), [makeFetch]),
+ const request: Req = useMemo(() => Object.defineProperties({
+ get: makeFetch(HTTPMethod.GET),
post,
- patch: useCallback(makeFetch(HTTPMethod.PATCH), [makeFetch]),
- put: useCallback(makeFetch(HTTPMethod.PUT), [makeFetch]),
+ patch: makeFetch(HTTPMethod.PATCH),
+ put: makeFetch(HTTPMethod.PUT),
del,
delete: del,
abort: () => controller.current && controller.current.abort(),
- query: (query, variables) => post({ query, variables }),
- mutate: (mutation, variables) => post({ mutation, variables }),
- loading,
- error: error.current,
- data: data.current,
+ query: (query: any, variables: any) => post({ query, variables }),
+ mutate: (mutation: any, variables: any) => post({ mutation, variables }),
cache
- }
+ }, {
+ loading: { get: () => loading.current },
+ error: { get: () => error.current },
+ data: { get: () => data.current },
+ }), [makeFetch])
const response = useMemo(() => toResponseObject(res, data), [])
@@ -254,8 +259,8 @@ function useFetch(...args: UseFetchArgs): UseFetch {
}
}
return Object.assign, UseFetchObjectReturn>(
- [request, response, loading, error.current],
- { request, response, ...request }
+ [request, response, loading.current, error.current],
+ { request, response, ...request, loading: loading.current, data: data.current, error: error.current }
)
}
diff --git a/src/useFetchArgs.ts b/src/useFetchArgs.ts
index 47b36a4d..c5cf6642 100644
--- a/src/useFetchArgs.ts
+++ b/src/useFetchArgs.ts
@@ -1,64 +1,9 @@
-import { OptionsMaybeURL, NoUrlOptions, Flatten, CachePolicies, Interceptors, OverwriteGlobalOptions, Options, RetryOn, RetryDelay } from './types'
+import { OptionsMaybeURL, NoUrlOptions, CachePolicies, Interceptors, OverwriteGlobalOptions, Options, RetryOn, RetryDelay, UseFetchArgsReturn } from './types'
import { isString, isObject, invariant, pullOutRequestInit, isFunction, isPositiveNumber } from './utils'
import { useContext, useMemo } from 'react'
import FetchContext from './FetchContext'
+import defaults from './defaults'
-type UseFetchArgsReturn = {
- customOptions: {
- cacheLife: number
- cachePolicy: CachePolicies
- interceptors: Interceptors
- onAbort: () => void
- onNewData: (currData: any, newData: any) => any
- onTimeout: () => void
- path: string
- perPage: number
- persist: boolean
- retries: number
- retryDelay: RetryDelay
- retryOn: RetryOn | undefined
- suspense: boolean
- timeout: number
- url: string
- }
- requestInit: RequestInit
- defaults: {
- loading: boolean
- data?: any
- }
- dependencies?: any[]
-}
-
-export const useFetchArgsDefaults: UseFetchArgsReturn = {
- customOptions: {
- cacheLife: 0,
- cachePolicy: CachePolicies.CACHE_FIRST,
- interceptors: {},
- onAbort: () => { /* do nothing */ },
- onNewData: (currData: any, newData: any) => newData,
- onTimeout: () => { /* do nothing */ },
- path: '',
- perPage: 0,
- persist: false,
- retries: 0,
- retryDelay: 1000,
- retryOn: [],
- suspense: false,
- timeout: 0,
- url: '',
- },
- requestInit: { headers: {} },
- defaults: {
- data: undefined,
- loading: false
- },
- dependencies: undefined
-}
-
-export const defaults = Object.entries(useFetchArgsDefaults).reduce((acc, [key, value]) => {
- if (isObject(value)) return { ...acc, ...value }
- return { ...acc, [key]: value }
-}, {} as Flatten)
const useField = (
field: keyof OptionsMaybeURL | keyof NoUrlOptions,
@@ -171,6 +116,7 @@ export default function useFetchArgs(
...contextRequestInit,
...requestInit,
headers: {
+ ...defaults.headers,
...contextRequestInit.headers,
...requestInit.headers
}
diff --git a/src/utils.ts b/src/utils.ts
index 2ba89ea9..26c41c08 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -155,7 +155,7 @@ export const tryGetData = async (res: Response | undefined, defaultData: any) =>
data = (await response.text()) as any // FIXME: should not be `any` type
} catch (er) {}
}
- return (defaultData && isEmpty(data)) ? defaultData : data
+ return !isEmpty(defaultData) && isEmpty(data) ? defaultData : data
}
/**