Skip to content

Commit 1155333

Browse files
committed
[FIX] usePromiseSuspensible cache logic
1 parent 970b1d9 commit 1155333

7 files changed

Lines changed: 101 additions & 52 deletions

File tree

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -852,9 +852,9 @@ usePopover({ mode, onBeforeToggle, onToggle }: UsePopoverProps): UsePopoverResul
852852
853853
### usePromiseSuspensible
854854
855-
Hook to resolve promise with Suspense support. The component that uses it, it need to be wrapped with Suspense component. [See demo](https://ndriadev.github.io/react-tools/#/hooks/api-dom/usePromiseSuspensible)
855+
Hook to resolve promise with Suspense support. The component that uses it, it need to be wrapped with Suspense component. This hook can be used in conditional blocks. [See demo](https://ndriadev.github.io/react-tools/#/hooks/api-dom/usePromiseSuspensible)
856856
```tsx
857-
usePromiseSuspensible<T extends (...args: unknown[]) => Promise<unknown>>(promise: T, deps: DependencyList, options: { clearCacheOnUnmount?: boolean } = {clearCacheOnUnmount: false}): Awaited<ReturnType<T>>
857+
usePromiseSuspensible<T>(promise: ()=>Promise<T>, deps: DependencyList, options: { cache?: "unmount" | number, cleanOnError?: boolean } = {}): Awaited<ReturnType<typeof promise>>
858858
```
859859
860860
### usePublishSubscribe
@@ -1281,10 +1281,17 @@ To validate dependencies of custom hooks like `useMemoCompare`, configure `exhau
12811281
"rules": {
12821282
// ...
12831283
"react-hooks/exhaustive-deps": [
1284-
"warn", {
1284+
"warn",
1285+
{
12851286
"additionalHooks": "(useMemoCompare|useMemoDeepCompare|useCallbackCompare|useCallbackDeepCompare|useLayoutEffectCompare|useLayoutEffectDeepCompare|useInsertionEffectCompare|useInsertionEffectDeepCompare|useEffectCompare|useEffectDeepCompare|usePromiseSuspensible)"
12861287
}
12871288
]
1289+
"react-hooks/rules-of-hooks": [
1290+
"of",
1291+
{
1292+
"additionalHooks": "(usePromiseSuspensible)"
1293+
}
1294+
]
12881295
}
12891296
}
12901297
```

apps/react-tools-demo/.eslintrc.cjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ module.exports = {
2525
{
2626
"additionalHooks": "(useMemoCompare|useMemoDeepCompare|useCallbackCompare|useCallbackDeepCompare|useLayoutEffectCompare|useLayoutEffectDeepCompare|useInsertionEffectCompare|useInsertionEffectDeepCompare|useEffectCompare|useEffectDeepCompare|usePromiseSuspensible)"
2727
}
28+
],
29+
"react-hooks/rules-of-hooks": [
30+
"off",
31+
{
32+
"additionalHooks": "(usePromiseSuspensible)"
33+
}
2834
]
2935
},
3036
}

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

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,29 @@ Hook to resolve promise with Suspense support. The component that uses it, it ne
55

66
```tsx
77
const Delayed = () => {
8-
const data = usePromiseSuspensible(() => {
9-
return new Promise<number[]>((res, rej) => {
10-
console.log("called")
11-
setTimeout(() => {
12-
Math.random() > 0.5
13-
? res([1, 2, 3, 4, 5])
14-
: rej();
15-
},4000)
16-
}).catch(() => alert("Error throwed by promise"))
17-
}, []);
18-
19-
return <pre>{JSON.stringify(data)}</pre>;
8+
const data = usePromiseSuspensible(
9+
() => {
10+
return new Promise<number[]>((res, rej) => {
11+
console.log("called")
12+
setTimeout(() => {
13+
Math.random() > 0.5
14+
? res([1, 2, 3, 4, 5])
15+
: rej("Error throwed by promise");
16+
},4000)
17+
}).catch((err) => {
18+
alert(err);
19+
return [0,0,0]
20+
})
21+
},
22+
[],
23+
{
24+
cache: "unmount"
25+
}
26+
);
27+
28+
return <>
29+
<pre>{JSON.stringify(data)}</pre>
30+
</>;
2031
}
2132

2233
export const UsePromiseSuspensible = () => {
@@ -32,7 +43,7 @@ export const UsePromiseSuspensible = () => {
3243
## API
3344

3445
```tsx
35-
usePromiseSuspensible<T extends (...args: unknown[]) => Promise<unknown>>(promise: T, deps: DependencyList, options: { clearCacheOnUnmount?: boolean } = {clearCacheOnUnmount: false}): Awaited<ReturnType<T>>
46+
usePromiseSuspensible<T extends (...args: unknown[]) => Promise<unknown>>(promise: T, deps: DependencyList, options: { cache?: "unmount" | number } = {}): Awaited<ReturnType<T>>
3647
```
3748
3849
@@ -44,8 +55,8 @@ Function that returns a promise to suspense.
4455
DependencyList for promise to suspense.
4556
> - __options?__: _{ clearCacheOnUnmount?: boolean }_
4657
optional options.
47-
> - __options.clearCacheOnUnmount=false?__: _boolean_
48-
if value is true, promise cached will be cleaned on unmount phase.
58+
> - __options.cache=undefined?__: _"unmount"|number_
59+
value can be "unmount", to clean promise cached at component unmounting, or it can be the duration in millisecond of cached promise.
4960
>
5061
5162
Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
1-
import { Suspense } from "react"
2-
import { usePromiseSuspensible } from "../../../../../../../packages/react-tools-lib/src";
1+
import { Suspense, useCallback } from "react"
2+
import { ErrorBoundary, usePromiseSuspensible } from "../../../../../../../packages/react-tools-lib/src";
33

44
/**
55
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(
9-
() => {
10-
return new Promise<number[]>((res, rej) => {
11-
console.log("called")
9+
async () => {
10+
return await new Promise<number[]>((res, rej) => {
11+
console.log("called");
1212
setTimeout(() => {
1313
Math.random() > 0.5
1414
? res([1, 2, 3, 4, 5])
1515
: rej("Error throwed by promise");
16-
},4000)
17-
}).catch((err) => {
18-
alert(err);
19-
throw err;
20-
})
16+
}, 4000);
17+
});
2118
},
2219
[],
2320
{
24-
cache: "unmount"
21+
cache: 25, //25 seconds
22+
cleanOnError: true
2523
}
2624
);
2725

@@ -31,7 +29,13 @@ const Delayed = () => {
3129
}
3230

3331
export const UsePromiseSuspensible = () => {
34-
return <Suspense fallback="loading...">
35-
<Delayed/>
36-
</Suspense>
32+
const fallback = useCallback<(error: Error, info: React.ErrorInfo, retry: () => void) => React.ReactNode>((_, __, retry) => {
33+
return <button onClick={retry}>Retry</button>
34+
}, []);
35+
36+
return <ErrorBoundary fallback={fallback}>
37+
<Suspense fallback="loading...">
38+
<Delayed />
39+
</Suspense>
40+
</ErrorBoundary>
3741
}

packages/react-tools-lib/README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -852,9 +852,9 @@ usePopover({ mode, onBeforeToggle, onToggle }: UsePopoverProps): UsePopoverResul
852852
853853
### usePromiseSuspensible
854854
855-
Hook to resolve promise with Suspense support. The component that uses it, it need to be wrapped with Suspense component. [See demo](https://ndriadev.github.io/react-tools/#/hooks/api-dom/usePromiseSuspensible)
855+
Hook to resolve promise with Suspense support. The component that uses it, it need to be wrapped with Suspense component. This hook can be used in conditional blocks. [See demo](https://ndriadev.github.io/react-tools/#/hooks/api-dom/usePromiseSuspensible)
856856
```tsx
857-
usePromiseSuspensible<T extends (...args: unknown[]) => Promise<unknown>>(promise: T, deps: DependencyList, options: { clearCacheOnUnmount?: boolean } = {clearCacheOnUnmount: false}): Awaited<ReturnType<T>>
857+
usePromiseSuspensible<T>(promise: ()=>Promise<T>, deps: DependencyList, options: { cache?: "unmount" | number, cleanOnError?: boolean } = {}): Awaited<ReturnType<typeof promise>>
858858
```
859859
860860
### usePublishSubscribe
@@ -1281,10 +1281,17 @@ To validate dependencies of custom hooks like `useMemoCompare`, configure `exhau
12811281
"rules": {
12821282
// ...
12831283
"react-hooks/exhaustive-deps": [
1284-
"warn", {
1284+
"warn",
1285+
{
12851286
"additionalHooks": "(useMemoCompare|useMemoDeepCompare|useCallbackCompare|useCallbackDeepCompare|useLayoutEffectCompare|useLayoutEffectDeepCompare|useInsertionEffectCompare|useInsertionEffectDeepCompare|useEffectCompare|useEffectDeepCompare|usePromiseSuspensible)"
12861287
}
12871288
]
1289+
"react-hooks/rules-of-hooks": [
1290+
"of",
1291+
{
1292+
"additionalHooks": "(usePromiseSuspensible)"
1293+
}
1294+
]
12881295
}
12891296
}
12901297
```

packages/react-tools-lib/scripts/generateREADME.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,10 +604,17 @@ async function generateReadme() {
604604
' "rules": {',
605605
' // ...',
606606
' "react-hooks/exhaustive-deps": [',
607-
' "warn", {',
607+
' "warn",',
608+
' {',
608609
' "additionalHooks": "(useMemoCompare|useMemoDeepCompare|useCallbackCompare|useCallbackDeepCompare|useLayoutEffectCompare|useLayoutEffectDeepCompare|useInsertionEffectCompare|useInsertionEffectDeepCompare|useEffectCompare|useEffectDeepCompare|usePromiseSuspensible)"',
609610
' }',
610611
' ]',
612+
' "react-hooks/rules-of-hooks": [',
613+
' "of",',
614+
' {',
615+
' "additionalHooks": "(usePromiseSuspensible)"',
616+
' }',
617+
' ]',
611618
' }',
612619
'}',
613620
"```",
Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1-
import { DependencyList, useRef } from "react";
1+
import { DependencyList } from "react";
22
import { useEffectOnce } from "../lifecycle";
33
import { isDeepEqual } from "../../utils";
44

5-
const promiseCache: { deps: DependencyList, promise: Promise<void>, error?: unknown, response?: unknown, cache: "unmount" | number | null }[] = [];
5+
const promiseCache: { deps: DependencyList, promise: Promise<void>, error?: unknown, response?: unknown, cache: "unmount" | number | null, errorTimeout?: number }[] = [];
66

77
/**
8-
* **`usePromiseSuspensible`**: Hook to resolve promise with Suspense support. The component that uses it, it need to be wrapped with Suspense component. [See demo](https://ndriadev.github.io/react-tools/#/hooks/api-dom/usePromiseSuspensible)
9-
* @param {T} promise - Function that returns a promise to suspense.
8+
* **`usePromiseSuspensible`**: Hook to resolve promise with Suspense support. The component that uses it, it need to be wrapped with Suspense component. This hook can be used in conditional blocks. [See demo](https://ndriadev.github.io/react-tools/#/hooks/api-dom/usePromiseSuspensible)
9+
* @param {()=>Promise<T>} promise - Function that returns a promise to suspense.
1010
* @param {DependencyList} deps - DependencyList for promise to suspense.
11-
* @param {{ clearCacheOnUnmount?: boolean }} [options] - optional options.
12-
* @param {"unmount"|number} [options.cache=undefined] - value can be "unmount", to clean promise cached at component unmounting, or it can be the duration in millisecond of cached promise.
11+
* @param {{ clearCacheOnUnmount?: "unmount"|number, cleanOnError?: boolean }} [options] - optional options.
12+
* @param {"unmount"|number} [options.cache=undefined] - value can be "unmount", to clean promise cached at component unmounting, or it can be the duration in __seconds__ of cached promise.
13+
* @param {boolean} [options.cleanOnError=undefined] - if true, when there is an error, remove promise from cache with a delay of 20 millisecond (due to multiple renders of react strict mode).
1314
* @returns {Awaited<ReturnType<T>>} result - resolve promise value.
1415
*/
15-
export const usePromiseSuspensible = <T extends (...args: unknown[]) => Promise<unknown>>(promise: T, deps: DependencyList, options: { cache?: "unmount" | number } = {}): Awaited<ReturnType<T>> => {
16-
const index = useRef(-1);
16+
export const usePromiseSuspensible = <T>(promise: ()=>Promise<T>, deps: DependencyList, options: { cache?: "unmount" | number, cleanOnError?: boolean } = {}): Awaited<ReturnType<typeof promise>> => {
17+
let index = -1;
1718
useEffectOnce(() => () => {
1819
if (options.cache === "unmount") {
19-
index.current !== -1 && promiseCache.splice(index.current, 1);
20-
index.current = -1;
20+
index !== -1 && promiseCache.splice(index, 1);
21+
index = -1;
2122
}
2223
})
2324
for (const ind in promiseCache) {
@@ -26,29 +27,35 @@ export const usePromiseSuspensible = <T extends (...args: unknown[]) => Promise<
2627
promiseCache.splice(Number(ind), 1);
2728
break;
2829
} else {
29-
index.current = Number(ind);
30+
index = Number(ind);
3031
if ("error" in promiseCache[ind]) {
32+
if (options.cleanOnError) {
33+
promiseCache[ind].errorTimeout !== -1 && clearTimeout(promiseCache[ind].errorTimeout);
34+
promiseCache[ind].errorTimeout = setTimeout(() => {
35+
promiseCache.splice(Number(ind), 1);
36+
}, 20) as unknown as number;
37+
}
3138
throw promiseCache[ind].error;
3239
}
3340
if ("response" in promiseCache[ind]) {
34-
return promiseCache[ind].response as Awaited<ReturnType<T>>;
41+
return promiseCache[ind].response as Awaited<ReturnType<typeof promise>>;
3542
}
3643
throw promiseCache[ind].promise;
3744
}
3845
}
3946
}
40-
const cached: { deps: DependencyList, promise: Promise<void>, error?: unknown, response?: Awaited<ReturnType<T>>, cache: "unmount" | number | null } = {
47+
const cached: { deps: DependencyList, promise: Promise<void>, error?: unknown, response?: Awaited<ReturnType<typeof promise>>, cache: "unmount" | number | null } = {
4148
deps: [...deps, String.raw`${promise.toString()}`],
42-
cache: options.cache ? options.cache === "unmount" ? "unmount" : Date.now() + options.cache : null,
49+
cache: options.cache ? options.cache === "unmount" ? "unmount" : Date.now() + (options.cache*1000) : null,
4350
promise: promise()
4451
.then(response => {
45-
cached.response = response as Awaited<ReturnType<T>>;
52+
cached.response = response as Awaited<ReturnType<typeof promise>>;
4653
})
4754
.catch(err => {
4855
cached.error = err;
4956
})
5057
};
5158

52-
index.current = promiseCache.push(cached) - 1;
59+
index = promiseCache.push(cached) - 1;
5360
throw cached.promise;
5461
}

0 commit comments

Comments
 (0)