1
1
import { useCallback , useEffect , useRef , useState } from 'react' ;
2
2
3
+ type UnknownResult = unknown ;
4
+
5
+ // Convenient to avoid declaring the type of args, which may help reduce type boilerplate
6
+ //type UnknownArgs = unknown[];
7
+ // TODO unfortunately it seems required for now if we want default param to work...
8
+ // See https://twitter.com/sebastienlorber/status/1170003594894106624
9
+ type UnknownArgs = any [ ] ;
10
+
11
+ export type AsyncStateStatus =
12
+ | 'not-requested'
13
+ | 'loading'
14
+ | 'success'
15
+ | 'error' ;
16
+
3
17
export type AsyncState < R > = {
18
+ status : AsyncStateStatus ;
4
19
loading : boolean ;
5
20
error : Error | undefined ;
6
21
result : R | undefined ;
@@ -11,46 +26,74 @@ type SetError<R> = (error: Error, asyncState: AsyncState<R>) => AsyncState<R>;
11
26
12
27
type MaybePromise < T > = Promise < T > | T ;
13
28
29
+ type PromiseCallbackOptions = {
30
+ // Permit to know if the success/error belongs to the last async call
31
+ isCurrent : ( ) => boolean ;
32
+
33
+ // TODO this can be convenient but need some refactor
34
+ // params: Args;
35
+ } ;
36
+
14
37
export type UseAsyncOptionsNormalized < R > = {
15
- initialState : ( ) => AsyncState < R > ;
38
+ initialState : ( options ?: UseAsyncOptionsNormalized < R > ) => AsyncState < R > ;
16
39
executeOnMount : boolean ;
17
40
executeOnUpdate : boolean ;
18
41
setLoading : SetLoading < R > ;
19
42
setResult : SetResult < R > ;
20
43
setError : SetError < R > ;
44
+ onSuccess : ( r : R , options : PromiseCallbackOptions ) => void ;
45
+ onError : ( e : Error , options : PromiseCallbackOptions ) => void ;
21
46
} ;
22
47
export type UseAsyncOptions < R > =
23
48
| Partial < UseAsyncOptionsNormalized < R > >
24
49
| undefined
25
50
| null ;
26
51
27
52
const InitialAsyncState : AsyncState < any > = {
53
+ status : 'not-requested' ,
54
+ loading : false ,
55
+ result : undefined ,
56
+ error : undefined ,
57
+ } ;
58
+
59
+ const InitialAsyncLoadingState : AsyncState < any > = {
60
+ status : 'loading' ,
28
61
loading : true ,
29
62
result : undefined ,
30
63
error : undefined ,
31
64
} ;
32
65
33
- const defaultSetLoading : SetLoading < any > = _asyncState => InitialAsyncState ;
66
+ const defaultSetLoading : SetLoading < any > = _asyncState =>
67
+ InitialAsyncLoadingState ;
34
68
35
69
const defaultSetResult : SetResult < any > = ( result , _asyncState ) => ( {
70
+ status : 'success' ,
36
71
loading : false ,
37
72
result : result ,
38
73
error : undefined ,
39
74
} ) ;
40
75
41
76
const defaultSetError : SetError < any > = ( error , _asyncState ) => ( {
77
+ status : 'error' ,
42
78
loading : false ,
43
79
result : undefined ,
44
80
error : error ,
45
81
} ) ;
46
82
47
- const DefaultOptions = {
48
- initialState : ( ) => InitialAsyncState ,
83
+ const noop = ( ) => { } ;
84
+
85
+ const DefaultOptions : UseAsyncOptionsNormalized < any > = {
86
+ initialState : options =>
87
+ options && options . executeOnMount
88
+ ? InitialAsyncLoadingState
89
+ : InitialAsyncState ,
49
90
executeOnMount : true ,
50
91
executeOnUpdate : true ,
51
92
setLoading : defaultSetLoading ,
52
93
setResult : defaultSetResult ,
53
94
setError : defaultSetError ,
95
+ onSuccess : noop ,
96
+ onError : noop ,
54
97
} ;
55
98
56
99
const normalizeOptions = < R > (
@@ -72,10 +115,10 @@ const useAsyncState = <R extends {}>(
72
115
options : UseAsyncOptionsNormalized < R >
73
116
) : UseAsyncStateResult < R > => {
74
117
const [ value , setValue ] = useState < AsyncState < R > > ( ( ) =>
75
- options . initialState ( )
118
+ options . initialState ( options )
76
119
) ;
77
120
78
- const reset = useCallback ( ( ) => setValue ( options . initialState ( ) ) , [
121
+ const reset = useCallback ( ( ) => setValue ( options . initialState ( options ) ) , [
79
122
setValue ,
80
123
options ,
81
124
] ) ;
@@ -130,26 +173,27 @@ const useCurrentPromise = <R>(): UseCurrentPromiseReturn<R> => {
130
173
} ;
131
174
132
175
export type UseAsyncReturn <
133
- R ,
134
- // never because most of the time we don't need manual execution feature (mostly useful for useAsyncCallback)
135
- // yet being able to declare the type easily
136
- Args extends any [ ] = never
176
+ R = UnknownResult ,
177
+ Args extends any [ ] = UnknownArgs
137
178
> = AsyncState < R > & {
138
179
set : ( value : AsyncState < R > ) => void ;
139
180
reset : ( ) => void ;
140
181
execute : ( ...args : Args ) => Promise < R > ;
141
182
currentPromise : Promise < R > | null ;
183
+ currentParams : Args | null ;
142
184
} ;
143
185
144
186
// Relaxed interface which accept both async and sync functions
145
187
// Accepting sync function is convenient for useAsyncCallback
146
- const useAsyncInternal = < R , Args extends any [ ] > (
188
+ const useAsyncInternal = < R = UnknownResult , Args extends any [ ] = UnknownArgs > (
147
189
asyncFunction : ( ...args : Args ) => MaybePromise < R > ,
148
190
params : Args ,
149
191
options ?: UseAsyncOptions < R >
150
192
) : UseAsyncReturn < R , Args > => {
151
193
const normalizedOptions = normalizeOptions < R > ( options ) ;
152
194
195
+ const [ currentParams , setCurrentParams ] = useState < Args | null > ( null ) ;
196
+
153
197
const AsyncState = useAsyncState < R > ( normalizedOptions ) ;
154
198
155
199
const isMounted = useIsMounted ( ) ;
@@ -162,6 +206,7 @@ const useAsyncInternal = <R, Args extends any[]>(
162
206
163
207
const executeAsyncOperation = ( ...args : Args ) : Promise < R > => {
164
208
const promise : MaybePromise < R > = asyncFunction ( ...args ) ;
209
+ setCurrentParams ( args ) ;
165
210
if ( promise instanceof Promise ) {
166
211
CurrentPromise . set ( promise ) ;
167
212
AsyncState . setLoading ( ) ;
@@ -170,11 +215,17 @@ const useAsyncInternal = <R, Args extends any[]>(
170
215
if ( shouldHandlePromise ( promise ) ) {
171
216
AsyncState . setResult ( result ) ;
172
217
}
218
+ normalizedOptions . onSuccess ( result , {
219
+ isCurrent : ( ) => CurrentPromise . is ( promise ) ,
220
+ } ) ;
173
221
} ,
174
222
error => {
175
223
if ( shouldHandlePromise ( promise ) ) {
176
224
AsyncState . setError ( error ) ;
177
225
}
226
+ normalizedOptions . onError ( error , {
227
+ isCurrent : ( ) => CurrentPromise . is ( promise ) ,
228
+ } ) ;
178
229
}
179
230
) ;
180
231
return promise ;
@@ -192,6 +243,7 @@ const useAsyncInternal = <R, Args extends any[]>(
192
243
useEffect ( ( ) => {
193
244
if ( isMounting ) {
194
245
normalizedOptions . executeOnMount && executeAsyncOperation ( ...params ) ;
246
+ normalizedOptions . executeOnMount && executeAsyncOperation ( ...params ) ;
195
247
} else {
196
248
normalizedOptions . executeOnUpdate && executeAsyncOperation ( ...params ) ;
197
249
}
@@ -203,23 +255,24 @@ const useAsyncInternal = <R, Args extends any[]>(
203
255
reset : AsyncState . reset ,
204
256
execute : executeAsyncOperation ,
205
257
currentPromise : CurrentPromise . get ( ) ,
258
+ currentParams,
206
259
} ;
207
260
} ;
208
261
209
262
// override to allow passing an async function with no args:
210
263
// gives more user-freedom to create an inline async function
211
- export function useAsync < R , Args extends any [ ] > (
264
+ export function useAsync < R = UnknownResult , Args extends any [ ] = UnknownArgs > (
212
265
asyncFunction : ( ) => Promise < R > ,
213
266
params : Args ,
214
267
options ?: UseAsyncOptions < R >
215
268
) : UseAsyncReturn < R , Args > ;
216
- export function useAsync < R , Args extends any [ ] > (
269
+ export function useAsync < R = UnknownResult , Args extends any [ ] = UnknownArgs > (
217
270
asyncFunction : ( ...args : Args ) => Promise < R > ,
218
271
params : Args ,
219
272
options ?: UseAsyncOptions < R >
220
273
) : UseAsyncReturn < R , Args > ;
221
274
222
- export function useAsync < R , Args extends any [ ] > (
275
+ export function useAsync < R = UnknownResult , Args extends any [ ] = UnknownArgs > (
223
276
asyncFunction : ( ...args : Args ) => Promise < R > ,
224
277
params : Args ,
225
278
options ?: UseAsyncOptions < R >
@@ -233,7 +286,10 @@ type AddArg<H, T extends any[]> = ((h: H, ...t: T) => void) extends ((
233
286
? R
234
287
: never ;
235
288
236
- export const useAsyncAbortable = < R , Args extends any [ ] > (
289
+ export const useAsyncAbortable = <
290
+ R = UnknownResult ,
291
+ Args extends any [ ] = UnknownArgs
292
+ > (
237
293
asyncFunction : ( ...args : AddArg < AbortSignal , Args > ) => Promise < R > ,
238
294
params : Args ,
239
295
options ?: UseAsyncOptions < R >
@@ -267,22 +323,34 @@ export const useAsyncAbortable = <R, Args extends any[]>(
267
323
return useAsync ( asyncFunctionWrapper , params , options ) ;
268
324
} ;
269
325
270
- export const useAsyncCallback = < R , Args extends any [ ] > (
271
- asyncFunction : ( ...args : Args ) => MaybePromise < R >
326
+ // keep compat with TS < 3.5
327
+ type LegacyOmit < T , K extends keyof T > = Pick < T , Exclude < keyof T , K > > ;
328
+
329
+ // Some options are not allowed for useAsyncCallback
330
+ export type UseAsyncCallbackOptions < R > =
331
+ | LegacyOmit <
332
+ Partial < UseAsyncOptionsNormalized < R > > ,
333
+ 'executeOnMount' | 'executeOnUpdate' | 'initialState'
334
+ >
335
+ | undefined
336
+ | null ;
337
+
338
+ export const useAsyncCallback = <
339
+ R = UnknownResult ,
340
+ Args extends any [ ] = UnknownArgs
341
+ > (
342
+ asyncFunction : ( ...args : Args ) => MaybePromise < R > ,
343
+ options ?: UseAsyncCallbackOptions < R >
272
344
) : UseAsyncReturn < R , Args > => {
273
345
return useAsyncInternal (
274
346
asyncFunction ,
275
347
// Hacky but in such case we don't need the params,
276
348
// because async function is only executed manually
277
349
[ ] as any ,
278
350
{
351
+ ...options ,
279
352
executeOnMount : false ,
280
353
executeOnUpdate : false ,
281
- initialState : ( ) => ( {
282
- loading : false ,
283
- result : undefined ,
284
- error : undefined ,
285
- } ) ,
286
354
}
287
355
) ;
288
356
} ;
0 commit comments