Skip to content

Commit 501f538

Browse files
committed
- status
- onSuccess / onError - useAsyncCallback options - currentParams
1 parent 7e513a3 commit 501f538

File tree

2 files changed

+91
-23
lines changed

2 files changed

+91
-23
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-async-hook",
3-
"version": "3.4.0",
3+
"version": "3.5.2",
44
"description": "Async hook",
55
"author": "Sébastien Lorber",
66
"license": "MIT",

src/index.ts

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
import { useCallback, useEffect, useRef, useState } from 'react';
22

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+
317
export type AsyncState<R> = {
18+
status: AsyncStateStatus;
419
loading: boolean;
520
error: Error | undefined;
621
result: R | undefined;
@@ -11,46 +26,74 @@ type SetError<R> = (error: Error, asyncState: AsyncState<R>) => AsyncState<R>;
1126

1227
type MaybePromise<T> = Promise<T> | T;
1328

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+
1437
export type UseAsyncOptionsNormalized<R> = {
15-
initialState: () => AsyncState<R>;
38+
initialState: (options?: UseAsyncOptionsNormalized<R>) => AsyncState<R>;
1639
executeOnMount: boolean;
1740
executeOnUpdate: boolean;
1841
setLoading: SetLoading<R>;
1942
setResult: SetResult<R>;
2043
setError: SetError<R>;
44+
onSuccess: (r: R, options: PromiseCallbackOptions) => void;
45+
onError: (e: Error, options: PromiseCallbackOptions) => void;
2146
};
2247
export type UseAsyncOptions<R> =
2348
| Partial<UseAsyncOptionsNormalized<R>>
2449
| undefined
2550
| null;
2651

2752
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',
2861
loading: true,
2962
result: undefined,
3063
error: undefined,
3164
};
3265

33-
const defaultSetLoading: SetLoading<any> = _asyncState => InitialAsyncState;
66+
const defaultSetLoading: SetLoading<any> = _asyncState =>
67+
InitialAsyncLoadingState;
3468

3569
const defaultSetResult: SetResult<any> = (result, _asyncState) => ({
70+
status: 'success',
3671
loading: false,
3772
result: result,
3873
error: undefined,
3974
});
4075

4176
const defaultSetError: SetError<any> = (error, _asyncState) => ({
77+
status: 'error',
4278
loading: false,
4379
result: undefined,
4480
error: error,
4581
});
4682

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,
4990
executeOnMount: true,
5091
executeOnUpdate: true,
5192
setLoading: defaultSetLoading,
5293
setResult: defaultSetResult,
5394
setError: defaultSetError,
95+
onSuccess: noop,
96+
onError: noop,
5497
};
5598

5699
const normalizeOptions = <R>(
@@ -72,10 +115,10 @@ const useAsyncState = <R extends {}>(
72115
options: UseAsyncOptionsNormalized<R>
73116
): UseAsyncStateResult<R> => {
74117
const [value, setValue] = useState<AsyncState<R>>(() =>
75-
options.initialState()
118+
options.initialState(options)
76119
);
77120

78-
const reset = useCallback(() => setValue(options.initialState()), [
121+
const reset = useCallback(() => setValue(options.initialState(options)), [
79122
setValue,
80123
options,
81124
]);
@@ -130,26 +173,27 @@ const useCurrentPromise = <R>(): UseCurrentPromiseReturn<R> => {
130173
};
131174

132175
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
137178
> = AsyncState<R> & {
138179
set: (value: AsyncState<R>) => void;
139180
reset: () => void;
140181
execute: (...args: Args) => Promise<R>;
141182
currentPromise: Promise<R> | null;
183+
currentParams: Args | null;
142184
};
143185

144186
// Relaxed interface which accept both async and sync functions
145187
// Accepting sync function is convenient for useAsyncCallback
146-
const useAsyncInternal = <R, Args extends any[]>(
188+
const useAsyncInternal = <R = UnknownResult, Args extends any[] = UnknownArgs>(
147189
asyncFunction: (...args: Args) => MaybePromise<R>,
148190
params: Args,
149191
options?: UseAsyncOptions<R>
150192
): UseAsyncReturn<R, Args> => {
151193
const normalizedOptions = normalizeOptions<R>(options);
152194

195+
const [currentParams, setCurrentParams] = useState<Args | null>(null);
196+
153197
const AsyncState = useAsyncState<R>(normalizedOptions);
154198

155199
const isMounted = useIsMounted();
@@ -162,6 +206,7 @@ const useAsyncInternal = <R, Args extends any[]>(
162206

163207
const executeAsyncOperation = (...args: Args): Promise<R> => {
164208
const promise: MaybePromise<R> = asyncFunction(...args);
209+
setCurrentParams(args);
165210
if (promise instanceof Promise) {
166211
CurrentPromise.set(promise);
167212
AsyncState.setLoading();
@@ -170,11 +215,17 @@ const useAsyncInternal = <R, Args extends any[]>(
170215
if (shouldHandlePromise(promise)) {
171216
AsyncState.setResult(result);
172217
}
218+
normalizedOptions.onSuccess(result, {
219+
isCurrent: () => CurrentPromise.is(promise),
220+
});
173221
},
174222
error => {
175223
if (shouldHandlePromise(promise)) {
176224
AsyncState.setError(error);
177225
}
226+
normalizedOptions.onError(error, {
227+
isCurrent: () => CurrentPromise.is(promise),
228+
});
178229
}
179230
);
180231
return promise;
@@ -192,6 +243,7 @@ const useAsyncInternal = <R, Args extends any[]>(
192243
useEffect(() => {
193244
if (isMounting) {
194245
normalizedOptions.executeOnMount && executeAsyncOperation(...params);
246+
normalizedOptions.executeOnMount && executeAsyncOperation(...params);
195247
} else {
196248
normalizedOptions.executeOnUpdate && executeAsyncOperation(...params);
197249
}
@@ -203,23 +255,24 @@ const useAsyncInternal = <R, Args extends any[]>(
203255
reset: AsyncState.reset,
204256
execute: executeAsyncOperation,
205257
currentPromise: CurrentPromise.get(),
258+
currentParams,
206259
};
207260
};
208261

209262
// override to allow passing an async function with no args:
210263
// 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>(
212265
asyncFunction: () => Promise<R>,
213266
params: Args,
214267
options?: UseAsyncOptions<R>
215268
): UseAsyncReturn<R, Args>;
216-
export function useAsync<R, Args extends any[]>(
269+
export function useAsync<R = UnknownResult, Args extends any[] = UnknownArgs>(
217270
asyncFunction: (...args: Args) => Promise<R>,
218271
params: Args,
219272
options?: UseAsyncOptions<R>
220273
): UseAsyncReturn<R, Args>;
221274

222-
export function useAsync<R, Args extends any[]>(
275+
export function useAsync<R = UnknownResult, Args extends any[] = UnknownArgs>(
223276
asyncFunction: (...args: Args) => Promise<R>,
224277
params: Args,
225278
options?: UseAsyncOptions<R>
@@ -233,7 +286,10 @@ type AddArg<H, T extends any[]> = ((h: H, ...t: T) => void) extends ((
233286
? R
234287
: never;
235288

236-
export const useAsyncAbortable = <R, Args extends any[]>(
289+
export const useAsyncAbortable = <
290+
R = UnknownResult,
291+
Args extends any[] = UnknownArgs
292+
>(
237293
asyncFunction: (...args: AddArg<AbortSignal, Args>) => Promise<R>,
238294
params: Args,
239295
options?: UseAsyncOptions<R>
@@ -267,22 +323,34 @@ export const useAsyncAbortable = <R, Args extends any[]>(
267323
return useAsync(asyncFunctionWrapper, params, options);
268324
};
269325

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>
272344
): UseAsyncReturn<R, Args> => {
273345
return useAsyncInternal(
274346
asyncFunction,
275347
// Hacky but in such case we don't need the params,
276348
// because async function is only executed manually
277349
[] as any,
278350
{
351+
...options,
279352
executeOnMount: false,
280353
executeOnUpdate: false,
281-
initialState: () => ({
282-
loading: false,
283-
result: undefined,
284-
error: undefined,
285-
}),
286354
}
287355
);
288356
};

0 commit comments

Comments
 (0)