-
Notifications
You must be signed in to change notification settings - Fork 105
Expand file tree
/
Copy pathindex.ts
More file actions
134 lines (122 loc) · 3.73 KB
/
index.ts
File metadata and controls
134 lines (122 loc) · 3.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import {useMemo, useRef, useState} from 'react';
import {useSyncedRef} from '../useSyncedRef/index.js';
export type AsyncStatus = 'loading' | 'success' | 'error' | 'not-executed';
export type AsyncState<Result> =
| {
status: 'not-executed';
error: undefined;
result: Result;
}
| {
status: 'success';
error: undefined;
result: Result;
}
| {
status: 'error';
error: Error;
result: Result;
}
| {
status: AsyncStatus;
error: Error | undefined;
result: Result;
};
export type UseAsyncActions<Result, Args extends unknown[] = unknown[]> = {
/**
* Reset state to initial.
*/
reset: () => void;
/**
* Execute the async function manually.
*/
execute: (...args: Args) => Promise<Result>;
};
export type UseAsyncMeta<Result, Args extends unknown[] = unknown[]> = {
/**
* Latest promise returned from the async function.
*/
promise: Promise<Result> | undefined;
/**
* List of arguments applied to the latest async function invocation.
*/
lastArgs: Args | undefined;
};
export function useAsync<Result, Args extends unknown[] = unknown[]>(
asyncFn: (...params: Args) => Promise<Result>,
initialValue: Result,
): [AsyncState<Result>, UseAsyncActions<Result, Args>, UseAsyncMeta<Result, Args>];
export function useAsync<Result, Args extends unknown[] = unknown[]>(
asyncFn: (...params: Args) => Promise<Result>,
initialValue?: Result,
): [AsyncState<Result | undefined>, UseAsyncActions<Result, Args>, UseAsyncMeta<Result, Args>];
/**
* Tracks the result and errors of the provided async function and provides handles to control its execution.
*
* @param asyncFn Function that returns a promise.
* @param initialValue Value that will be set on initialisation before the async function is
* executed.
*/
export function useAsync<Result, Args extends unknown[] = unknown[]>(
asyncFn: (...params: Args) => Promise<Result>,
initialValue?: Result,
): [AsyncState<Result | undefined>, UseAsyncActions<Result, Args>, UseAsyncMeta<Result, Args>] {
const [state, setState] = useState<AsyncState<Result | undefined>>({
status: 'not-executed',
error: undefined,
result: initialValue,
});
const promiseRef = useRef<Promise<Result>>(undefined);
const argsRef = useRef<Args>(undefined);
const methods = useSyncedRef({
async execute(...params: Args) {
argsRef.current = params;
const promise = asyncFn(...params);
promiseRef.current = promise;
setState((s) => ({...s, status: 'loading'}));
// eslint-disable-next-line promise/catch-or-return, promise/prefer-await-to-then
promise.then(
(result) => {
// We dont want to handle result/error of non-latest function
// this approach helps to avoid race conditions
// eslint-disable-next-line promise/always-return
if (promise === promiseRef.current) {
setState((s) => ({...s, status: 'success', error: undefined, result}));
}
},
// eslint-disable-next-line @typescript-eslint/use-unknown-in-catch-callback-variable
(error: Error) => {
// We don't want to handle result/error of non-latest function
// this approach helps to avoid race conditions
if (promise === promiseRef.current) {
setState((previousState) => ({...previousState, status: 'error', error}));
}
},
);
return promise;
},
reset() {
setState({
status: 'not-executed',
error: undefined,
result: initialValue,
});
promiseRef.current = undefined;
argsRef.current = undefined;
},
});
return [
state,
useMemo(
() => ({
reset() {
methods.current.reset();
},
execute: async (...params: Args) => methods.current.execute(...params),
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
),
{promise: promiseRef.current, lastArgs: argsRef.current},
];
}