Skip to content

Commit 2b09d9c

Browse files
committed
Allow overriding the 'resource' argument of 'fetch' when invoking 'run'.
1 parent 16b9385 commit 2b09d9c

File tree

4 files changed

+69
-22
lines changed

4 files changed

+69
-22
lines changed

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -695,13 +695,20 @@ throw an exception and crash if the promise rejects.
695695
696696
Runs the `deferFn`, passing any arguments provided as an array.
697697

698-
When used with `useFetch`, `run` has a different signature:
698+
When used with `useFetch`, `run` has several overloaded signatures:
699+
700+
> `function(resource: String | Resource, init: Object | (init: Object) => Object): void`
699701
700702
> `function(init: Object | (init: Object) => Object): void`
701703
702-
This runs the `fetch` request using the provided `init`. If it's an object it will be spread over the default `init`
703-
(`useFetch`'s 2nd argument). If it's a function it will be invoked with the default `init` and should return a new
704-
`init` object. This way you can either extend or override the value of `init`, for example to set request headers.
704+
> `function(event: SyntheticEvent | Event): void`
705+
706+
> `function(): void`
707+
708+
This way you can run the `fetch` request using the provided `resource` and `init`. `resource` can be omitted. If `init`
709+
is an object it will be spread over the default `init` (`useFetch`'s 2nd argument). If it's a function it will be
710+
invoked with the default `init` and should return a new `init` object. This way you can either extend or override the
711+
value of `init`, for example to set request headers.
705712

706713
#### `reload`
707714

packages/react-async/src/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ type AsyncInitialWithout<K extends keyof AsyncInitial<T>, T> =
227227
| Omit<AsyncRejected<T>, K>
228228

229229
type FetchRun<T> = {
230+
run(overrideResource: RequestInfo, overrideInit: (init: RequestInit) => RequestInit): void
231+
run(overrideResource: RequestInfo, overrideInit: Partial<RequestInit>): void
230232
run(overrideInit: (init: RequestInit) => RequestInit): void
231233
run(overrideInit: Partial<RequestInit>): void
232234
run(ignoredEvent: React.SyntheticEvent): void

packages/react-async/src/useAsync.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -173,27 +173,31 @@ const parseResponse = (accept, json) => res => {
173173
return accept === "application/json" ? res.json() : res
174174
}
175175

176-
const useAsyncFetch = (input, init, { defer, json, ...options } = {}) => {
177-
const method = input.method || (init && init.method)
178-
const headers = input.headers || (init && init.headers) || {}
176+
const isResource = value => typeof value === "string" || (typeof value === "object" && value.url)
177+
178+
const useAsyncFetch = (resource, init, { defer, json, ...options } = {}) => {
179+
const method = resource.method || (init && init.method)
180+
const headers = resource.headers || (init && init.headers) || {}
179181
const accept = headers["Accept"] || headers["accept"] || (headers.get && headers.get("accept"))
180-
const doFetch = (input, init) => globalScope.fetch(input, init).then(parseResponse(accept, json))
182+
const doFetch = (resource, init) =>
183+
globalScope.fetch(resource, init).then(parseResponse(accept, json))
181184
const isDefer =
182185
typeof defer === "boolean" ? defer : ["POST", "PUT", "PATCH", "DELETE"].indexOf(method) !== -1
183186
const fn = isDefer ? "deferFn" : "promiseFn"
184-
const identity = JSON.stringify({ input, init, isDefer })
187+
const identity = JSON.stringify({ resource, init, isDefer })
185188
const state = useAsync({
186189
...options,
187190
[fn]: useCallback(
188191
(arg1, arg2, arg3) => {
189-
const [override, signal] = arg3 ? [arg1[0], arg3.signal] : [undefined, arg2.signal]
190-
if (typeof override === "object" && "preventDefault" in override) {
192+
const [runArgs, signal] = isDefer ? [arg1, arg3.signal] : [[], arg2.signal]
193+
const [runResource, runInit] = isResource(runArgs[0]) ? runArgs : [, runArgs[0]]
194+
if (typeof runInit === "object" && "preventDefault" in runInit) {
191195
// Don't spread Events or SyntheticEvents
192-
return doFetch(input, { signal, ...init })
196+
return doFetch(runResource || resource, { signal, ...init })
193197
}
194-
return typeof override === "function"
195-
? doFetch(input, { signal, ...override(init) })
196-
: doFetch(input, { signal, ...init, ...override })
198+
return typeof runInit === "function"
199+
? doFetch(runResource || resource, { signal, ...runInit(init) })
200+
: doFetch(runResource || resource, { signal, ...init, ...runInit })
197201
},
198202
[identity] // eslint-disable-line react-hooks/exhaustive-deps
199203
),

packages/react-async/src/useAsync.spec.js

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,11 @@ describe("useFetch", () => {
203203
expect(json).toHaveBeenCalled()
204204
})
205205

206-
test("calling `run` with a method argument allows to override `init` parameters", () => {
206+
test("calling `run` with a callback as argument allows to override `init` parameters", () => {
207207
const component = (
208-
<Fetch input="/test" init={{ method: "POST" }}>
208+
<Fetch input="/test" init={{ method: "POST", body: '{"name":"foo"}' }}>
209209
{({ run }) => (
210-
<button onClick={() => run(init => ({ ...init, body: '{"name":"test"}' }))}>run</button>
210+
<button onClick={() => run(init => ({ ...init, body: '{"name":"bar"}' }))}>run</button>
211211
)}
212212
</Fetch>
213213
)
@@ -216,22 +216,56 @@ describe("useFetch", () => {
216216
fireEvent.click(getByText("run"))
217217
expect(globalScope.fetch).toHaveBeenCalledWith(
218218
"/test",
219-
expect.objectContaining({ method: "POST", signal: abortCtrl.signal, body: '{"name":"test"}' })
219+
expect.objectContaining({ method: "POST", signal: abortCtrl.signal, body: '{"name":"bar"}' })
220220
)
221221
})
222222

223223
test("calling `run` with an object as argument allows to override `init` parameters", () => {
224224
const component = (
225-
<Fetch input="/test" init={{ method: "POST" }}>
226-
{({ run }) => <button onClick={() => run({ body: '{"name":"test"}' })}>run</button>}
225+
<Fetch input="/test" init={{ method: "POST", body: '{"name":"foo"}' }}>
226+
{({ run }) => <button onClick={() => run({ body: '{"name":"bar"}' })}>run</button>}
227227
</Fetch>
228228
)
229229
const { getByText } = render(component)
230230
expect(globalScope.fetch).not.toHaveBeenCalled()
231231
fireEvent.click(getByText("run"))
232232
expect(globalScope.fetch).toHaveBeenCalledWith(
233233
"/test",
234-
expect.objectContaining({ method: "POST", signal: abortCtrl.signal, body: '{"name":"test"}' })
234+
expect.objectContaining({ method: "POST", signal: abortCtrl.signal, body: '{"name":"bar"}' })
235+
)
236+
})
237+
238+
test("calling `run` with a url allows to override fetch's `resource` parameter", () => {
239+
const component = (
240+
<Fetch input="/foo" options={{ defer: true }}>
241+
{({ run }) => <button onClick={() => run("/bar")}>run</button>}
242+
</Fetch>
243+
)
244+
const { getByText } = render(component)
245+
expect(globalScope.fetch).not.toHaveBeenCalled()
246+
fireEvent.click(getByText("run"))
247+
expect(globalScope.fetch).toHaveBeenCalledWith(
248+
"/bar",
249+
expect.objectContaining({ signal: abortCtrl.signal })
250+
)
251+
})
252+
253+
test("overriding the `resource` can be combined with overriding `init`", () => {
254+
const component = (
255+
<Fetch input="/foo" init={{ method: "POST", body: '{"name":"foo"}' }}>
256+
{({ run }) => (
257+
<button onClick={() => run("/bar", init => ({ ...init, body: '{"name":"bar"}' }))}>
258+
run
259+
</button>
260+
)}
261+
</Fetch>
262+
)
263+
const { getByText } = render(component)
264+
expect(globalScope.fetch).not.toHaveBeenCalled()
265+
fireEvent.click(getByText("run"))
266+
expect(globalScope.fetch).toHaveBeenCalledWith(
267+
"/bar",
268+
expect.objectContaining({ method: "POST", signal: abortCtrl.signal, body: '{"name":"bar"}' })
235269
)
236270
})
237271

0 commit comments

Comments
 (0)