Skip to content

Commit c502dcb

Browse files
committed
Add 'defer' and 'json' options to useFetch.
1 parent d3fbb17 commit c502dcb

File tree

4 files changed

+84
-11
lines changed

4 files changed

+84
-11
lines changed

README.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,11 @@ const MyComponent = () => {
181181
}
182182
```
183183

184-
`useFetch` takes the same arguments as [fetch] itself, as well as options to the underlying `useAsync` hook. `useFetch`
185-
automatically uses `promiseFn` or `deferFn` based on the request method (`deferFn` for POST / PUT / PATCH / DELETE) and
186-
handles JSON parsing if the `Accept` header is set to `"application/json"`.
184+
`useFetch` takes the same arguments as [fetch] itself, as well as `options` to the underlying `useAsync` hook. The
185+
`options` object takes two special boolean properties: `defer` and `json`. These can be used to switch between
186+
`deferFn` and `promiseFn`, and enable JSON parsing. By default `useFetch` automatically uses `promiseFn` or `deferFn`
187+
based on the request method (`deferFn` for POST / PUT / PATCH / DELETE) and handles JSON parsing if the `Accept` header
188+
is set to `"application/json"`.
187189

188190
[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
189191

@@ -287,6 +289,11 @@ These can be passed in an object to `useAsync()`, or as props to `<Async>` and c
287289
- `onResolve` Callback invoked when Promise resolves.
288290
- `onReject` Callback invoked when Promise rejects.
289291

292+
`useFetch` additionally takes these options:
293+
294+
- `defer` Force the use of `deferFn` or `promiseFn`.
295+
- `json` Enable JSON parsing of the response.
296+
290297
#### `promise`
291298

292299
> `Promise`
@@ -354,6 +361,20 @@ Callback function invoked when a promise resolves, receives data as argument.
354361
355362
Callback function invoked when a promise rejects, receives rejection reason (error) as argument.
356363

364+
#### `defer`
365+
366+
> `boolean`
367+
368+
Enables the use of `deferFn` if `true`, or enables the use of `promiseFn` if `false`. By default this is automatically
369+
chosen based on the request method (`deferFn` for POST / PUT / PATCH / DELETE, `promiseFn` otherwise).
370+
371+
#### `json`
372+
373+
> `boolean`
374+
375+
Enables or disables JSON parsing of the response body. By default this is automatically enabled if the `Accept` header
376+
is set to `"application/json"`.
377+
357378
### Render props
358379

359380
`<Async>` provides the following render props to the `children` function:

src/index.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,14 @@ interface FetchInit {
6666
headers?: Headers | object
6767
}
6868

69+
interface FetchOptions<T> extends AsyncOptions<T> {
70+
defer?: boolean
71+
}
72+
6973
export function useFetch<T>(
7074
input: Request | string,
7175
init?: FetchInit,
72-
options?: AsyncOptions<T>
76+
options?: FetchOptions<T>
7377
): AsyncState<T>
7478

7579
export default createInstance

src/useAsync.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,18 +117,20 @@ const useAsync = (arg1, arg2) => {
117117
)
118118
}
119119

120-
const parseResponse = accept => res => {
120+
const parseResponse = (accept, json) => res => {
121121
if (!res.ok) return Promise.reject(res)
122-
if (accept === "application/json") return res.json()
122+
if (json === false) return res
123+
if (json === true || accept === "application/json") return res.json()
123124
return res
124125
}
125126

126-
const useAsyncFetch = (input, init, options) => {
127+
const useAsyncFetch = (input, init, { defer, json, ...options } = {}) => {
127128
const method = input.method || (init && init.method)
128129
const headers = input.headers || (init && init.headers) || {}
129130
const accept = headers["Accept"] || headers["accept"] || (headers.get && headers.get("accept"))
130-
const doFetch = (input, init) => window.fetch(input, init).then(parseResponse(accept))
131-
const fn = ~["POST", "PUT", "PATCH", "DELETE"].indexOf(method) ? "deferFn" : "promiseFn"
131+
const doFetch = (input, init) => window.fetch(input, init).then(parseResponse(accept, json))
132+
const isDefer = defer === true || ~["POST", "PUT", "PATCH", "DELETE"].indexOf(method)
133+
const fn = defer === false || !isDefer ? "promiseFn" : "deferFn"
132134
const state = useAsync({
133135
...options,
134136
[fn]: useCallback(

src/useAsync.spec.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ window.fetch = jest.fn(() => Promise.resolve({ ok: true, json }))
1414

1515
beforeEach(abortCtrl.abort.mockClear)
1616
beforeEach(window.fetch.mockClear)
17+
beforeEach(json.mockClear)
1718
afterEach(cleanup)
1819

1920
const Async = ({ children = () => null, ...props }) => children(useAsync(props))
20-
const Fetch = ({ children = () => null, input, init, ...props }) =>
21-
children(useFetch(input, init, props))
21+
const Fetch = ({ children = () => null, input, init, options }) =>
22+
children(useFetch(input, init, options))
2223

2324
describe("useAsync", () => {
2425
describe("common", common(Async, abortCtrl))
@@ -67,9 +68,54 @@ describe("useFetch", () => {
6768
)
6869
})
6970

71+
test("defer=true uses deferFn", () => {
72+
const component = (
73+
<Fetch input="/test" options={{ defer: true }}>
74+
{({ run }) => <button onClick={run}>run</button>}
75+
</Fetch>
76+
)
77+
const { getByText } = render(component)
78+
expect(window.fetch).not.toHaveBeenCalled()
79+
fireEvent.click(getByText("run"))
80+
expect(window.fetch).toHaveBeenCalledWith(
81+
"/test",
82+
expect.objectContaining({ signal: abortCtrl.signal })
83+
)
84+
})
85+
86+
test("defer=false uses promiseFn", () => {
87+
render(
88+
<Fetch input="/test" init={{ method: "POST" }} options={{ defer: false }}>
89+
{({ run }) => <button onClick={run}>run</button>}
90+
</Fetch>
91+
)
92+
expect(window.fetch).toHaveBeenCalledWith(
93+
"/test",
94+
expect.objectContaining({ method: "POST", signal: abortCtrl.signal })
95+
)
96+
})
97+
7098
test("automatically handles JSON parsing", async () => {
7199
render(<Fetch input="/test" init={{ headers: { accept: "application/json" } }} />)
72100
await Promise.resolve()
73101
expect(json).toHaveBeenCalled()
74102
})
103+
104+
test("json=false disables JSON parsing", async () => {
105+
render(
106+
<Fetch
107+
input="/test"
108+
init={{ headers: { accept: "application/json" } }}
109+
options={{ json: false }}
110+
/>
111+
)
112+
await Promise.resolve()
113+
expect(json).not.toHaveBeenCalled()
114+
})
115+
116+
test("json=true enables JSON parsing", async () => {
117+
render(<Fetch input="/test" options={{ json: true }} />)
118+
await Promise.resolve()
119+
expect(json).toHaveBeenCalled()
120+
})
75121
})

0 commit comments

Comments
 (0)