import {useMemo, useRef, useState} from 'react'; import {useSyncedRef} from '../useSyncedRef/index.js'; export type AsyncStatus = 'loading' | 'success' | 'error' | 'not-executed'; export type AsyncState = | { status: 'not-executed'; error: undefined; result: Result; } | { status: 'success'; error: undefined; result: Result; } | { status: 'error'; error: Error; result: Result; } | { status: AsyncStatus; error: Error | undefined; result: Result; }; export type UseAsyncActions = { /** * Reset state to initial. */ reset: () => void; /** * Execute the async function manually. */ execute: (...args: Args) => Promise; }; export type UseAsyncMeta = { /** * Latest promise returned from the async function. */ promise: Promise | undefined; /** * List of arguments applied to the latest async function invocation. */ lastArgs: Args | undefined; }; export function useAsync( asyncFn: (...params: Args) => Promise, initialValue: Result, ): [AsyncState, UseAsyncActions, UseAsyncMeta]; export function useAsync( asyncFn: (...params: Args) => Promise, initialValue?: Result, ): [AsyncState, UseAsyncActions, UseAsyncMeta]; /** * Tracks the result and errors of the provided async function and provides handles to control its execution. * * @param asyncFn Function that returns a promise. * @param initialValue Value that will be set on initialisation before the async function is * executed. */ export function useAsync( asyncFn: (...params: Args) => Promise, initialValue?: Result, ): [AsyncState, UseAsyncActions, UseAsyncMeta] { const [state, setState] = useState>({ status: 'not-executed', error: undefined, result: initialValue, }); const promiseRef = useRef>(undefined); const argsRef = useRef(undefined); const methods = useSyncedRef({ async execute(...params: Args) { argsRef.current = params; const promise = asyncFn(...params); promiseRef.current = promise; setState((s) => ({...s, status: 'loading'})); // eslint-disable-next-line promise/catch-or-return, promise/prefer-await-to-then promise.then( (result) => { // We dont want to handle result/error of non-latest function // this approach helps to avoid race conditions // eslint-disable-next-line promise/always-return if (promise === promiseRef.current) { setState((s) => ({...s, status: 'success', error: undefined, result})); } }, // eslint-disable-next-line @typescript-eslint/use-unknown-in-catch-callback-variable (error: Error) => { // We don't want to handle result/error of non-latest function // this approach helps to avoid race conditions if (promise === promiseRef.current) { setState((previousState) => ({...previousState, status: 'error', error})); } }, ); return promise; }, reset() { setState({ status: 'not-executed', error: undefined, result: initialValue, }); promiseRef.current = undefined; argsRef.current = undefined; }, }); return [ state, useMemo( () => ({ reset() { methods.current.reset(); }, execute: async (...params: Args) => methods.current.execute(...params), }), // eslint-disable-next-line react-hooks/exhaustive-deps [], ), {promise: promiseRef.current, lastArgs: argsRef.current}, ]; }