Files
cinny/src/app/hooks/useAsyncCallback.ts
T

95 lines
2.6 KiB
TypeScript
Raw Normal View History

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()) {
setState({
status: AsyncStatus.Success,
data,
});
}
return data;
} catch (e) {
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()) {
setState({
status: AsyncStatus.Error,
2024-01-21 23:50:56 +11:00
error: e as TError,
2023-06-12 21:15:23 +10:00
});
}
throw e;
}
},
[asyncCallback, alive]
);
return [state, callback];
};