Skip to content

Commit 2053176

Browse files
committed
[IMPL] useFetch hook
1 parent 6f2e9a1 commit 2053176

10 files changed

Lines changed: 227 additions & 11 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Suspense } from "react";
2+
import { useFetch } from "../../../../../../packages/react-tools/src";
3+
4+
/**
5+
The component uses _useFetch_ hook to call jsonplaceholder API with suspense support and without it.
6+
*/
7+
export const Delayed = () => {
8+
const [data, callSuspensable] = useFetch("https://jsonplaceholder.typicode.com/comments?id=5", { cache: "no-cache", suspensable: true });
9+
const [data1, call, loading, error] = useFetch("https://jsonplaceholder.typicode.com/comments?id=5", { cache: "no-cache" });
10+
11+
if (loading ) {
12+
return <p>Loading...</p>
13+
}
14+
if (error) {
15+
return <p>Error: {JSON.stringify(error)}</p>
16+
}
17+
return <>
18+
<button onClick={() => callSuspensable()}>Call suspensable</button>
19+
<button onClick={() => call()}>Call</button>
20+
<pre>{JSON.stringify(data??data1, null, 2)}</pre>
21+
</>
22+
}
23+
24+
export const UseFetch = () => {
25+
return <Suspense fallback="loading suspense...">
26+
<Delayed />
27+
</Suspense>
28+
}

apps/react-tools-demo/src/components/hooks/usePromiseSuspensible/UsePromiseSuspensible.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Suspense } from "react"
22
import { usePromiseSuspensible } from "../../../../../../packages/react-tools/src";
33

44
/**
5-
The _Delayed_ component uses _usePromiseSuspensible_ to call a promise that resolves with an array of number or reject: if promise has been resolved, array number is rendered, otherwise an alert is invocked. Delayed component is returned from _UsePromiseSuspensible_ component.
5+
The _Delayed_ component uses _usePromiseSuspensible_ hook to call a promise that resolves with an array of number or reject: if promise has been resolved, array number is rendered, otherwise an alert is invocked. Delayed component is returned from _UsePromiseSuspensible_ component.
66
*/
77
const Delayed = () => {
88
const data = usePromiseSuspensible(() => {

apps/react-tools-demo/src/constants/components.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ export const COMPONENTS = [
118118
"useDisplayMedia",
119119
"useWebWorker",
120120
"useWebWorkerFn",
121-
"usePromiseSuspensible"
121+
"usePromiseSuspensible",
122+
"useFetch"
122123
]
123124
],
124125
//UTILS
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# useFetch
2+
Hook to use [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) with more control and the possibility to execute request with suspense support.
3+
4+
## Usage
5+
6+
```tsx
7+
export const Delayed = () => {
8+
const [data, callSuspensable] = useFetch("https://jsonplaceholder.typicode.com/comments?id=5", { cache: "no-cache", suspensable: true });
9+
const [data1, call, loading, error] = useFetch("https://jsonplaceholder.typicode.com/comments?id=5", { cache: "no-cache" });
10+
11+
if (loading ) {
12+
return <p>Loading...</p>
13+
}
14+
if (error) {
15+
return <p>Error: {JSON.stringify(error)}</p>
16+
}
17+
return <>
18+
<button onClick={() => callSuspensable()}>Call suspensable</button>
19+
<button onClick={() => call()}>Call</button>
20+
<pre>{JSON.stringify(data??data1, null, 2)}</pre>
21+
</>
22+
}
23+
24+
export const UseFetch = () => {
25+
return <Suspense fallback="loading suspense...">
26+
<Delayed />
27+
</Suspense>
28+
}
29+
```
30+
31+
> The component uses _useFetch_ hook to call jsonplaceholder API with suspense support and without it.
32+
33+
34+
## API
35+
36+
```tsx
37+
useFetch<T>(url: RequestInfo | URL, { suspensable, onError, onLoading, ...rest }: RequestInit & { suspensable?: boolean, onLoading?: (loading: boolean) => void, onError?: (err: unknown) => void } = {}): [T|undefined, (conf?: RequestInit) => Promise<void>, boolean, unknown]
38+
```
39+
40+
> ### Params
41+
>
42+
> - __url__: _RequestInfo|URL_
43+
The resource that you wish to fetch. This can either be a string, a Request object or an URL object.
44+
> - __options?__: _Object_
45+
An object containing any custom settings you want to apply to the fetch invokation.
46+
> - __...options.rest?__: _RequestInit_
47+
properties to customize fetch settings.
48+
> - __options.onLoading?__: _(loading: boolean)=>void_
49+
function that will be executed when loading state changes.
50+
> - __options.onError?__: _(error:unknown)=>void_
51+
function that will be executed when error occurred.
52+
> - __options.suspensable?__: _boolean_
53+
boolean that indicates if fetch request need to be suspends or not.
54+
>
55+
56+
> ### Returns
57+
>
58+
> : __Array__:
59+
- _T|undefined_
60+
- _(conf?: RequestInit) => Promise<void>_
61+
- _boolean_
62+
- _unknown_
63+
> Array with:
64+
> - __data__: data returned from fetch.
65+
> - __call__: function to fetch request.
66+
> - __loading__: value that handle loading state.
67+
> - __error__: value that handle error state.
68+
>

apps/react-tools-demo/src/markdown/usePromiseSuspensible.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ export const UsePromiseSuspensible = () => {
2626
}
2727
```
2828

29-
> The _Delayed_ component uses _usePromiseSuspensible_ to call a promise that resolves with an array of number or reject: if promise has been resolved, array number is rendered, otherwise an alert is invocked. Delayed component is returned from _UsePromiseSuspensible_ component.
29+
> The _Delayed_ component uses _usePromiseSuspensible_ hook to call a promise that resolves with an array of number or reject: if promise has been resolved, array number is rendered, otherwise an alert is invocked. Delayed component is returned from _UsePromiseSuspensible_ component.
3030
3131

3232
## API
3333

3434
```tsx
35-
usePromiseSuspensible<T extends (...args: unknown[]) => Promise<unknown>>(promise: T, deps: DependencyList)
35+
usePromiseSuspensible<T extends (...args: unknown[]) => Promise<unknown>>(promise: T, deps: DependencyList): Awaited<ReturnType<T>>
3636
```
3737
3838
> ### Params

packages/react-tools/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@
118118
- [x] useDisplayMedia
119119
- [x] useWebWorker
120120
- [x] useWebWorkerFn
121-
- [ ] usePromiseSuspensible
122-
- [ ] useFetch (with suspense ???)
121+
- [x] usePromiseSuspensible
122+
- [x] useFetch
123123
- [ ] useLock - (https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request)
124124
- [ ] useIndexedDB (TODO)
125125
- [ ] useIdleDetection (not work yet. https://developer.mozilla.org/en-US/docs/Web/API/Idle_Detection_API)

packages/react-tools/src/hooks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,5 @@ export { useDisplayMedia } from './useDisplayMedia';
9999
export { useSwipe } from './useSwipe';
100100
export { useWebWorker } from './useWebWorker';
101101
export { useWebWorkerFn } from './useWebWorkerFn';
102-
export { usePromiseSuspensible } from './usePromiseSuspensible';
102+
export { usePromiseSuspensible } from './usePromiseSuspensible';
103+
export { useFetch } from './useFetch';
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { useCallback, useRef, useState } from "react"
2+
3+
/**
4+
* **`useFetch`**: Hook to use [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) with more control and the possibility to execute request with suspense support.
5+
* @param {RequestInfo|URL} url - The resource that you wish to fetch. This can either be a string, a Request object or an URL object.
6+
* @param {Object} [options] - An object containing any custom settings you want to apply to the fetch invokation.
7+
* @param {RequestInit} [...options.rest] - properties to customize fetch settings.
8+
* @param {(loading: boolean)=>void} [options.onLoading] - function that will be executed when loading state changes.
9+
* @param {(error:unknown)=>void} [options.onError] - function that will be executed when error occurred.
10+
* @param {boolean} [options.suspensable] - boolean that indicates if fetch request need to be suspends or not.
11+
* @returns {[T|undefined, (conf?: RequestInit) => Promise<void>, boolean, unknown]}
12+
* Array with:
13+
* - __data__: data returned from fetch.
14+
* - __call__: function to fetch request.
15+
* - __loading__: value that handle loading state.
16+
* - __error__: value that handle error state.
17+
*/
18+
export const useFetch = <T>(url: RequestInfo | URL, { suspensable, onError, onLoading, ...rest }: RequestInit & { suspensable?: boolean, onLoading?: (loading: boolean) => void, onError?: (err: unknown) => void } = {}): [T|undefined, (conf?: RequestInit) => Promise<void>, boolean, unknown] => {
19+
const [status, setStatus] = useState<{ loading: boolean, error: unknown, data?: T, suspended: boolean }>({
20+
loading: false,
21+
error: undefined,
22+
data: undefined,
23+
suspended: false
24+
});
25+
const promise = useRef<Promise<void>>();
26+
27+
if (status.suspended) {
28+
throw promise.current;
29+
}
30+
31+
const call = useCallback((conf?: RequestInit) => {
32+
if (suspensable) {
33+
let response: Response;
34+
promise.current = fetch(url, { ...rest, ...conf })
35+
.then(resp => {
36+
response = resp;
37+
return (!response.headers.get("Content-Type") || response.headers.get("Content-Type")?.startsWith("text")
38+
? response.text()
39+
: response.headers.get("Content-Type")?.includes("json")
40+
? response.json()
41+
: response.blob()) as T;
42+
})
43+
.then(data => {
44+
if (response.ok) {
45+
setStatus({ error: undefined, loading: false, data, suspended: false });
46+
promise.current = undefined;
47+
!!onLoading && onLoading(false);
48+
} else {
49+
const error = data
50+
? data
51+
: response.status + " - " + response.statusText;
52+
setStatus({
53+
loading: false,
54+
data: undefined,
55+
error,
56+
suspended: false
57+
});
58+
promise.current = undefined;
59+
!!onLoading && onLoading(false);
60+
!!onError && onError(error);
61+
}
62+
})
63+
.catch(err => {
64+
const error = typeof err === "string"
65+
? err
66+
: (err as Error).message;
67+
setStatus({ error, data: undefined, loading: false, suspended: false });
68+
promise.current = undefined;
69+
!!onLoading && onLoading(false);
70+
!!onError && onError(error);
71+
});
72+
setStatus({loading: true, data: undefined, error: undefined, suspended: true});
73+
return promise.current;
74+
} else {
75+
let response: Response;
76+
!!onLoading && onLoading(true);
77+
setStatus({ loading: true, error: undefined, data: undefined, suspended: false });
78+
return fetch(url, { ...rest, ...conf })
79+
.then(resp => {
80+
response = resp;
81+
return (!response.headers.get("Content-Type") || response.headers.get("Content-Type")?.startsWith("text")
82+
? response.text()
83+
: response.headers.get("Content-Type")?.includes("json")
84+
? response.json()
85+
: response.blob()) as T;
86+
})
87+
.then(data => {
88+
if (response.ok) {
89+
setStatus({ error: undefined, loading: false, data, suspended: false });
90+
!!onLoading && onLoading(false);
91+
} else {
92+
const error = data
93+
? data
94+
: response.status + " - " + response.statusText;
95+
setStatus({
96+
loading: false,
97+
data: undefined,
98+
error,
99+
suspended: false
100+
});
101+
!!onLoading && onLoading(false);
102+
!!onError && onError(error);
103+
}
104+
})
105+
.catch(err => {
106+
const error = typeof err === "string"
107+
? err
108+
: (err as Error).message;
109+
setStatus({ error, data: undefined, loading: false, suspended: false });
110+
!!onLoading && onLoading(false);
111+
!!onError && onError(error);
112+
});
113+
}
114+
}, [rest, url, onError, onLoading, suspensable]);
115+
116+
return [status.data, call, status.loading, status.error];
117+
}

packages/react-tools/src/hooks/usePromiseSuspensible.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const promiseCache: { deps: DependencyList, promise: Promise<void>, error?: unkn
99
* @param {DependencyList} deps - DependencyList for promise to suspense.
1010
* @returns {Awaited<ReturnType<T>>} result - resolve promise value.
1111
*/
12-
export const usePromiseSuspensible = <T extends (...args: unknown[]) => Promise<unknown>>(promise: T, deps: DependencyList) => {
12+
export const usePromiseSuspensible = <T extends (...args: unknown[]) => Promise<unknown>>(promise: T, deps: DependencyList): Awaited<ReturnType<T>> => {
1313
const index = useRef(-1);
1414
useEffectOnce(() => () => {
1515
index.current !== -1 && promiseCache.splice(index.current, 1);
@@ -18,10 +18,10 @@ export const usePromiseSuspensible = <T extends (...args: unknown[]) => Promise<
1818
for (const cached of promiseCache) {
1919
index.current = index.current+1;
2020
if (isDeepEqual([...deps, promise.toString()], cached.deps)) {
21-
if (Object.prototype.hasOwnProperty.call(cached, "error")) {
21+
if ("error" in cached) {
2222
throw cached.error;
2323
}
24-
if (Object.prototype.hasOwnProperty.call(cached, "response")) {
24+
if ("response" in cached) {
2525
return cached.response as Awaited<ReturnType<T>>;
2626
}
2727
throw cached.promise;

packages/react-tools/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,8 @@ export {
196196
useSwipe,
197197
useWebWorker,
198198
useWebWorkerFn,
199-
usePromiseSuspensible
199+
usePromiseSuspensible,
200+
useFetch
200201
} from './hooks'
201202

202203
export {

0 commit comments

Comments
 (0)