@@ -195,3 +195,105 @@ export const useUpdateEffect = (callback: React.EffectCallback, deps: React.Depe
195
195
}
196
196
} , [ ] )
197
197
}
198
+
199
+ export type FunctionReturningPromise = ( ...args : any [ ] ) => Promise < any >
200
+
201
+ export const useAsync = < T extends FunctionReturningPromise > (
202
+ func : T ,
203
+ deps : React . DependencyList = [ ] ,
204
+ ) => {
205
+ const [ state , callback ] = useAsyncFunc ( func , deps , { loading : true } )
206
+
207
+ React . useEffect ( ( ) => {
208
+ callback ( )
209
+ } , [ callback ] )
210
+
211
+ return state
212
+ }
213
+
214
+ export type AsyncState < T > =
215
+ | {
216
+ loading : boolean
217
+ error ?: undefined
218
+ value ?: undefined
219
+ }
220
+ | {
221
+ loading : true
222
+ error ?: Error | undefined
223
+ value ?: T
224
+ }
225
+ | {
226
+ loading : false
227
+ error : Error
228
+ value ?: undefined
229
+ }
230
+ | {
231
+ loading : false
232
+ error ?: undefined
233
+ value : T
234
+ }
235
+
236
+ export type PromiseType < P extends Promise < any > > = P extends Promise < infer T > ? T : never
237
+
238
+ type StateFromFunctionReturningPromise < T extends FunctionReturningPromise > = AsyncState <
239
+ PromiseType < ReturnType < T > >
240
+ >
241
+
242
+ export type AsyncFnReturn < T extends FunctionReturningPromise = FunctionReturningPromise > = [
243
+ StateFromFunctionReturningPromise < T > ,
244
+ T ,
245
+ ]
246
+
247
+ export const useAsyncFunc = < T extends FunctionReturningPromise > (
248
+ func : T ,
249
+ deps : React . DependencyList = [ ] ,
250
+ initialState : StateFromFunctionReturningPromise < T > = { loading : false } ,
251
+ ) : AsyncFnReturn < T > => {
252
+ const lastCallId = React . useRef ( 0 )
253
+ const isMounted = useIsMounted ( )
254
+ const [ state , setState ] = React . useState < StateFromFunctionReturningPromise < T > > ( initialState )
255
+
256
+ const callback = React . useCallback ( ( ...args : Parameters < T > ) : ReturnType < T > => {
257
+ const callId = ++ lastCallId . current
258
+
259
+ if ( ! state . loading ) {
260
+ setState ( ( prevState ) => ( { ...prevState , loading : true } ) )
261
+ }
262
+
263
+ return func ( ...args ) . then (
264
+ ( value ) => {
265
+ if ( isMounted . current && callId === lastCallId . current ) setState ( { value, loading : false } )
266
+
267
+ return value
268
+ } ,
269
+ ( error ) => {
270
+ if ( isMounted . current && callId === lastCallId . current ) setState ( { error, loading : false } )
271
+
272
+ return error
273
+ } ,
274
+ ) as ReturnType < T >
275
+ // eslint-disable-next-line react-hooks/exhaustive-deps
276
+ } , deps )
277
+
278
+ return [ state , callback as unknown as T ]
279
+ }
280
+
281
+ export type AsyncStateRetry < T > = AsyncState < T > & {
282
+ retry ( ) : void
283
+ }
284
+
285
+ export const useAsyncRetry = < T , > ( func : ( ) => Promise < T > , deps : React . DependencyList = [ ] ) => {
286
+ const [ attempt , setAttempt ] = React . useState < number > ( 0 )
287
+ const state = useAsync ( func , [ ...deps , attempt ] )
288
+
289
+ const stateLoading = state . loading
290
+
291
+ const retry = React . useCallback ( ( ) => {
292
+ if ( stateLoading ) return
293
+
294
+ setAttempt ( ( currentAttempt ) => currentAttempt + 1 )
295
+ // eslint-disable-next-line react-hooks/exhaustive-deps
296
+ } , [ ...deps , stateLoading ] )
297
+
298
+ return { ...state , retry }
299
+ }
0 commit comments