Skip to content

Commit 3a07662

Browse files
authored
Merge pull request #39 from nebarf/unabortable-request
Unabortable request
2 parents 40b4341 + 72bbfa7 commit 3a07662

File tree

4 files changed

+140
-21
lines changed

4 files changed

+140
-21
lines changed

README.md

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<h1 align="center">React http fetch</h1>
22

33
<p align="center">
4-
<img src="assets/img/react-http-fetch-logo.png" alt="react-http-fetch logo"/>
4+
<img src="https://github.com/nebarf/react-http-fetch/blob/main/assets/img/react-http-fetch-logo.png?raw=true" alt="react-http-fetch logo"/>
55
<br>
66
<i>A http library for React JS built on top of JS native fetch.</i>
77
<br>
@@ -50,6 +50,8 @@ Just follow links below to get an overview of library features.
5050
- [Http request state](#http-request-state)
5151
- [Example &ndash; Http request hook triggered automatically on component mount](#example--http-request-hook-triggered-automatically-on-component-mount)
5252
- [Example &ndash; Http request hook triggered manually on component mount](#example--http-request-hook-triggered-manually-on-component-mount)
53+
- [Example &ndash; Non-abortable http request hook](#example--non-abortable-http-request-hook)
54+
- [Example &ndash; Aborting http request triggered by the hook](#example--aborting-http-request-triggered-by-the-hook)
5355
- [Example &ndash; Http post request hook](#example--http-post-request-hook)
5456
- [Events](#events)
5557
- [Caching](#caching)
@@ -347,7 +349,12 @@ The library provides a hook `useHttpRequest` managing the state of the http requ
347349
| fetchOnBootstrap | boolean | Tell if the fetch must be triggered automatically when mounting the component or not. In the second case we would like to have a manual fetch, this is optained by a request function returned by the hook. |
348350

349351
### Http request hook return
350-
Returns an array of two elements, the first one embeds the state of the http request and the second one is a function that can be used to trigger the http request. The table below describes the shape (i.e. properties) of http request state.
352+
Returns an array of three elements:
353+
- The first one embeds the state of the http request.
354+
- The second is a function that can be used to perform an abortable http request.
355+
- The third is a function that can be used to perform a non-abortable http request.
356+
357+
See examples for further details. The table below describes the shape (i.e. properties) of http request state.
351358

352359
### Http request state
353360
| Property | Type | Description |
@@ -373,7 +380,7 @@ function App() {
373380
});
374381

375382
return (
376-
<div>{`Todo name: ${state && state.data && state.data.title}`}</div>
383+
<div>{`Todo name: ${(state.data && state.data.title) || 'unknown'}`}</div>
377384
);
378385
}
379386

@@ -393,16 +400,99 @@ function App() {
393400
relativeUrl: 'todos/1',
394401
});
395402

403+
useEffect(() => {
404+
const { reqResult, abortController } = request();
405+
reqResult
406+
.then(res => console.log('request response', res))
407+
.catch(err => console.error(err));
408+
409+
// You can use the returned AbortController instance to abort the request
410+
// abortController.abort();
411+
}, [request]);
412+
413+
return (
414+
<div>{`Todo name: ${(state.data && state.data.title) || 'unknown'}`}</div>
415+
);
416+
}
417+
418+
export default App;
419+
```
420+
421+
### Example &ndash; Non-abortable http request hook
422+
```js
423+
Placeholder
424+
import React, { useEffect } from 'react';
425+
import { useHttpRequest } from 'react-http-fetch';
426+
427+
function App() {
428+
const [state, , request] = useHttpRequest({
429+
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
430+
relativeUrl: 'todos/1',
431+
});
432+
396433
useEffect(() => request(), [request]);
397434

398435
return (
399-
<div>{`Todo name: ${state && state.data && state.data.title}`}</div>
436+
<div>{`Todo name: ${(state.data && state.data.title) || 'unknown'}`}</div>
400437
);
401438
}
402439

403440
export default App;
404441
```
405442

443+
### Example &ndash; Aborting http request triggered by the hook
444+
```js
445+
import { useHttpRequest } from 'react-http-fetch';
446+
import React, { useRef } from 'react';
447+
448+
function App() {
449+
const [state, request] = useHttpRequest({
450+
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
451+
relativeUrl: 'todos/1',
452+
});
453+
454+
const abortCtrlRef = useRef();
455+
456+
const fetchTodo = () => {
457+
abortPendingRequest();
458+
459+
const { reqResult, abortController } = request();
460+
abortCtrlRef.current = abortController;
461+
462+
reqResult
463+
// Abort the request will cause the request promise to be rejected with the following error:
464+
// "DOMException: The user aborted a request."
465+
.catch(err => console.error(err));
466+
};
467+
468+
const abortPendingRequest = () => {
469+
if (abortCtrlRef.current) {
470+
abortCtrlRef.current.abort();
471+
}
472+
};
473+
474+
return (
475+
<div style={{ margin: '20px' }}>
476+
<div>{`Todo name: ${(state.data && state.data.title) || 'unknown'}`}</div>
477+
<button
478+
style={{ marginRight: '10px' }}
479+
type="button"
480+
onClick={fetchTodo}
481+
>
482+
Do request
483+
</button>
484+
<button
485+
type="button"
486+
onClick={abortPendingRequest}
487+
>
488+
Abort
489+
</button>
490+
</div>
491+
);
492+
}
493+
494+
export default App;
495+
```
406496
### Example &ndash; Http post request hook
407497

408498
```js

src/__tests__/use-http-request.spec.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('use-http-request', () => {
3737
wrapper: HttpClientProviderConfigFixture.create(),
3838
});
3939

40-
const [, performRequest] = result.current;
40+
const [, , performRequest] = result.current;
4141

4242
expect(result.current[0].data).toBeUndefined();
4343
expect(result.current[0].error).toBeNull();
@@ -47,8 +47,7 @@ describe('use-http-request', () => {
4747

4848
let reqResult: Promise<{ name: string; role: string }>;
4949
act(() => {
50-
const { reqResult: performRequestRes } = performRequest();
51-
reqResult = performRequestRes;
50+
reqResult = performRequest();
5251
});
5352

5453
expect(result.current[0].data).toBeUndefined();
@@ -83,7 +82,7 @@ describe('use-http-request', () => {
8382
expect(fetchHeaders).toEqual(defaultHttpReqConfig.reqOptions.headers);
8483
expect(fetchCredentials).toBeUndefined();
8584
expect(fetchBody).toBeNull();
86-
expect(fetchSignal).toBeInstanceOf(AbortSignal);
85+
expect(fetchSignal).toBeUndefined();
8786
});
8887

8988
test('should update the request state when it goes in error', async () => {
@@ -94,7 +93,7 @@ describe('use-http-request', () => {
9493
wrapper: HttpClientProviderConfigFixture.create(),
9594
});
9695

97-
const [, performRequest] = result.current;
96+
const [, , performRequest] = result.current;
9897

9998
expect(result.current[0].data).toBeUndefined();
10099
expect(result.current[0].error).toBeNull();
@@ -104,8 +103,7 @@ describe('use-http-request', () => {
104103

105104
let reqResult: Promise<unknown>;
106105
act(() => {
107-
const { reqResult: performRequestRes } = performRequest();
108-
reqResult = performRequestRes;
106+
reqResult = performRequest();
109107
});
110108

111109
expect(result.current[0].data).toBeUndefined();
@@ -149,7 +147,7 @@ describe('use-http-request', () => {
149147
expect(fetchHeaders).toEqual(defaultHttpReqConfig.reqOptions.headers);
150148
expect(fetchCredentials).toBeUndefined();
151149
expect(fetchBody).toBeNull();
152-
expect(fetchSignal).toBeInstanceOf(AbortSignal);
150+
expect(fetchSignal).toBeUndefined();
153151
});
154152

155153
test('should automatically perform the request on mount', async () => {
@@ -191,7 +189,7 @@ describe('use-http-request', () => {
191189
expect(fetchHeaders).toEqual(defaultHttpReqConfig.reqOptions.headers);
192190
expect(fetchCredentials).toBeUndefined();
193191
expect(fetchBody).toBeNull();
194-
expect(fetchSignal).toBeInstanceOf(AbortSignal);
192+
expect(fetchSignal).toBeUndefined();
195193
});
196194

197195
test('should merge params provided on hook declaration with the ones provided on request run', async () => {
@@ -214,7 +212,7 @@ describe('use-http-request', () => {
214212
}
215213
);
216214

217-
const [, performRequest] = result.current;
215+
const [, , performRequest] = result.current;
218216

219217
expect(result.current[0].data).toBeUndefined();
220218
expect(result.current[0].error).toBeNull();
@@ -224,8 +222,7 @@ describe('use-http-request', () => {
224222

225223
let reqResult: Promise<string>;
226224
act(() => {
227-
const { reqResult: performRequestRes } = performRequest();
228-
reqResult = performRequestRes;
225+
reqResult = performRequest();
229226
});
230227

231228
expect(result.current[0].data).toBeUndefined();
@@ -262,7 +259,7 @@ describe('use-http-request', () => {
262259
});
263260
expect(fetchCredentials).toBeUndefined();
264261
expect(fetchBody).toBeNull();
265-
expect(fetchSignal).toBeInstanceOf(AbortSignal);
262+
expect(fetchSignal).toBeUndefined();
266263
});
267264

268265
test('should allow to abort the request', async () => {

src/request/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ export interface UseHttpAbortableRequestReturn<HttpResponseT> {
1919

2020
export type UseHttpRequestReturn<HttpResponseT> = [
2121
HttpRequestState<HttpResponseT>,
22-
() => UseHttpAbortableRequestReturn<HttpResponseT>
22+
() => UseHttpAbortableRequestReturn<HttpResponseT>,
23+
() => Promise<HttpResponseT>
2324
];

src/request/use-http-request.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export const useHttpRequest = <HttpResponseT, HttpRequestBodyT = unknown>(
1212
/**
1313
* Grabs the "request" function from the http client.
1414
*/
15-
const { abortableRequest: httpClientAbortableRequest } = useHttpClient();
15+
const { abortableRequest: httpClientAbortableRequest, request: httpClientRequest } =
16+
useHttpClient();
1617

1718
// The state of the request.
1819
const [state, dispatch] = useReducer<Reducer<HttpRequestState<HttpResponseT>, HttpReqActionType>>(
@@ -89,7 +90,7 @@ export const useHttpRequest = <HttpResponseT, HttpRequestBodyT = unknown>(
8990
/**
9091
* Performs the http request allowing to abort it.
9192
*/
92-
const request = useCompareCallback(
93+
const abortableRequest = useCompareCallback(
9394
(
9495
paramsOverride?: Partial<PerformHttpRequestParams<HttpRequestBodyT, HttpResponseT>>
9596
): UseHttpAbortableRequestReturn<HttpResponseT> => {
@@ -119,6 +120,36 @@ export const useHttpRequest = <HttpResponseT, HttpRequestBodyT = unknown>(
119120
fastCompare
120121
);
121122

123+
/**
124+
* Performs the http request.
125+
*/
126+
const request = useCompareCallback(
127+
(
128+
paramsOverride?: Partial<PerformHttpRequestParams<HttpRequestBodyT, HttpResponseT>>
129+
): Promise<HttpResponseT> => {
130+
safelyDispatch(requestInit());
131+
132+
const mergedParams = paramsOverride
133+
? mergeParams(performHttpRequestParams, paramsOverride)
134+
: performHttpRequestParams;
135+
const reqResult = httpClientRequest<HttpResponseT, HttpRequestBodyT>(mergedParams);
136+
137+
// Listen request to be successfully resolved or reject and
138+
// update the state accordingly.
139+
reqResult
140+
.then((response) => {
141+
safelyDispatch(requestSuccess(response));
142+
})
143+
.catch((error) => {
144+
safelyDispatch(requestError(error));
145+
});
146+
147+
return reqResult;
148+
},
149+
[httpClientRequest, performHttpRequestParams, safelyDispatch],
150+
fastCompare
151+
);
152+
122153
/**
123154
* Keeps track of the mounting state of the component.
124155
*/
@@ -139,5 +170,5 @@ export const useHttpRequest = <HttpResponseT, HttpRequestBodyT = unknown>(
139170
fastCompare
140171
);
141172

142-
return [state, request];
173+
return [state, abortableRequest, request];
143174
};

0 commit comments

Comments
 (0)