2024-01-21 23:50:56 +11:00
|
|
|
import { useCallback, useRef, useState } from 'react';
|
|
|
|
|
import { flushSync } from 'react-dom';
|
2023-06-12 21:15:23 +10:00
|
|
|
import { useAlive } from './useAlive';
|
|
|
|
|
|
|
|
|
|
export enum AsyncStatus {
|
|
|
|
|
Idle = 'idle',
|
|
|
|
|
Loading = 'loading',
|
|
|
|
|
Success = 'success',
|
|
|
|
|
Error = 'error',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type AsyncIdle = {
|
|
|
|
|
status: AsyncStatus.Idle;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type AsyncLoading = {
|
|
|
|
|
status: AsyncStatus.Loading;
|
|
|
|
|
};
|
|
|
|
|
|
2024-01-21 23:50:56 +11:00
|
|
|
export type AsyncSuccess<D> = {
|
2023-06-12 21:15:23 +10:00
|
|
|
status: AsyncStatus.Success;
|
2024-01-21 23:50:56 +11:00
|
|
|
data: D;
|
2023-06-12 21:15:23 +10:00
|
|
|
};
|
|
|
|
|
|
2024-01-21 23:50:56 +11:00
|
|
|
export type AsyncError<E = unknown> = {
|
2023-06-12 21:15:23 +10:00
|
|
|
status: AsyncStatus.Error;
|
2024-01-21 23:50:56 +11:00
|
|
|
error: E;
|
2023-06-12 21:15:23 +10:00
|
|
|
};
|
|
|
|
|
|
2024-01-21 23:50:56 +11:00
|
|
|
export type AsyncState<D, E = unknown> = AsyncIdle | AsyncLoading | AsyncSuccess<D> | AsyncError<E>;
|
2023-06-12 21:15:23 +10:00
|
|
|
|
|
|
|
|
export type AsyncCallback<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData>;
|
|
|
|
|
|
2024-01-21 23:50:56 +11:00
|
|
|
export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
|
2023-06-12 21:15:23 +10:00
|
|
|
asyncCallback: AsyncCallback<TArgs, TData>
|
2024-01-21 23:50:56 +11:00
|
|
|
): [AsyncState<TData, TError>, AsyncCallback<TArgs, TData>] => {
|
|
|
|
|
const [state, setState] = useState<AsyncState<TData, TError>>({
|
2023-06-12 21:15:23 +10:00
|
|
|
status: AsyncStatus.Idle,
|
|
|
|
|
});
|
|
|
|
|
const alive = useAlive();
|
|
|
|
|
|
2024-01-21 23:50:56 +11:00
|
|
|
// Tracks the request number.
|
|
|
|
|
// If two or more requests are made subsequently
|
|
|
|
|
// we will throw all old request's response after they resolved.
|
|
|
|
|
const reqNumberRef = useRef(0);
|
|
|
|
|
|
2023-06-12 21:15:23 +10:00
|
|
|
const callback: AsyncCallback<TArgs, TData> = useCallback(
|
|
|
|
|
async (...args) => {
|
2024-01-21 23:50:56 +11:00
|
|
|
queueMicrotask(() => {
|
|
|
|
|
// Warning: flushSync was called from inside a lifecycle method.
|
|
|
|
|
// React cannot flush when React is already rendering.
|
|
|
|
|
// Consider moving this call to a scheduler task or micro task.
|
|
|
|
|
flushSync(() => {
|
|
|
|
|
// flushSync because
|
|
|
|
|
// https://github.com/facebook/react/issues/26713#issuecomment-1872085134
|
|
|
|
|
setState({
|
|
|
|
|
status: AsyncStatus.Loading,
|
|
|
|
|
});
|
|
|
|
|
});
|
2023-06-12 21:15:23 +10:00
|
|
|
});
|
|
|
|
|
|
2024-01-21 23:50:56 +11:00
|
|
|
reqNumberRef.current += 1;
|
|
|
|
|
|
|
|
|
|
const currentReqNumber = reqNumberRef.current;
|
2023-06-12 21:15:23 +10:00
|
|
|
try {
|
|
|
|
|
const data = await asyncCallback(...args);
|
2024-01-21 23:50:56 +11:00
|
|
|
if (currentReqNumber !== reqNumberRef.current) {
|
|
|
|
|
throw new Error('AsyncCallbackHook: Request replaced!');
|
|
|
|
|
}
|
2023-06-12 21:15:23 +10:00
|
|
|
if (alive()) {
|
2024-05-31 19:49:46 +05:30
|
|
|
queueMicrotask(() => {
|
|
|
|
|
setState({
|
|
|
|
|
status: AsyncStatus.Success,
|
|
|
|
|
data,
|
|
|
|
|
});
|
2023-06-12 21:15:23 +10:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
} catch (e) {
|
2024-01-21 23:50:56 +11:00
|
|
|
if (currentReqNumber !== reqNumberRef.current) {
|
|
|
|
|
throw new Error('AsyncCallbackHook: Request replaced!');
|
|
|
|
|
}
|
2024-05-31 19:49:46 +05:30
|
|
|
|
2023-06-12 21:15:23 +10:00
|
|
|
if (alive()) {
|
2024-05-31 19:49:46 +05:30
|
|
|
queueMicrotask(() => {
|
|
|
|
|
setState({
|
|
|
|
|
status: AsyncStatus.Error,
|
|
|
|
|
error: e as TError,
|
|
|
|
|
});
|
2023-06-12 21:15:23 +10:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[asyncCallback, alive]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return [state, callback];
|
|
|
|
|
};
|