-
Notifications
You must be signed in to change notification settings - Fork 96
/
Copy pathindex.ts
133 lines (121 loc) · 3.59 KB
/
index.ts
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
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>>();
const argsRef = useRef<Args>();
const methods = useSyncedRef({
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.then(
(result) => {
// We dont want to handle result/error of non-latest function
// this approach helps to avoid race conditions
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: (...params: Args) => methods.current.execute(...params),
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
),
{promise: promiseRef.current, lastArgs: argsRef.current},
];
}