import { useCallback, useEffect, useRef, useState } from 'react'; import { flushSync } from 'react-dom'; 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; }; export type AsyncSuccess = { status: AsyncStatus.Success; data: D; }; export type AsyncError = { status: AsyncStatus.Error; error: E; }; export type AsyncState = AsyncIdle | AsyncLoading | AsyncSuccess | AsyncError; export type AsyncCallback = (...args: TArgs) => Promise; export const useAsync = ( asyncCallback: AsyncCallback, onStateChange: (state: AsyncState) => void ): AsyncCallback => { const alive = useAlive(); // 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); const callback: AsyncCallback = useCallback( async (...args) => { 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 onStateChange({ status: AsyncStatus.Loading, }); }); }); reqNumberRef.current += 1; const currentReqNumber = reqNumberRef.current; try { const data = await asyncCallback(...args); if (currentReqNumber !== reqNumberRef.current) { throw new Error('AsyncCallbackHook: Request replaced!'); } if (alive()) { queueMicrotask(() => { onStateChange({ status: AsyncStatus.Success, data, }); }); } return data; } catch (e) { if (currentReqNumber !== reqNumberRef.current) { throw new Error('AsyncCallbackHook: Request replaced!'); } if (alive()) { queueMicrotask(() => { onStateChange({ status: AsyncStatus.Error, error: e as TError, }); }); } throw e; } }, [asyncCallback, alive, onStateChange] ); return callback; }; export const useAsyncCallback = ( asyncCallback: AsyncCallback ): [AsyncState, AsyncCallback] => { const [state, setState] = useState>({ status: AsyncStatus.Idle, }); const callback = useAsync(asyncCallback, setState); return [state, callback]; }; export const useAsyncCallbackValue = ( asyncCallback: AsyncCallback<[], TData> ): [AsyncState, AsyncCallback<[], TData>] => { const [state, load] = useAsyncCallback(asyncCallback); useEffect(() => { load(); }, [load]); return [state, load]; };