/** * Copied from https://github.com/react-hookz/web/blob/579a445fcc9f4f4bb5b9d5e670b2e57448b4ee50/src/useAsync/index.ts */ import { useMemo, useRef, useState } from 'react'; import { stringifyErrorLike } from '../utils/misc'; export type AsyncStatus = 'loading' | 'success' | 'error' | 'not-executed'; export type AsyncState = | AsyncStateUninitialized | AsyncStateFulfilled | AsyncStateWithError | AsyncStateLoading; export type AsyncStateWithError = { status: 'error'; error: Error; result: Result; }; export type AsyncStateFulfilled = { status: 'success'; error: undefined; result: Result; }; export type AsyncStateUninitialized = { status: 'not-executed'; error: undefined; result: Result; }; export type AsyncStateLoading = { status: 'loading'; 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 ): [UseAsyncActions, AsyncState, UseAsyncMeta]; export function useAsync( asyncFn: (...params: Args) => Promise, initialValue?: Result ): [UseAsyncActions, AsyncState, 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 ): [UseAsyncActions, AsyncState, UseAsyncMeta] { const [state, setState] = useState>({ status: 'not-executed', error: undefined, result: initialValue, }); const promiseRef = useRef>(); const argsRef = useRef(); const methods = useSyncedRef({ execute(...params: Args) { argsRef.current = params; const promise = asyncFn(...params); promiseRef.current = promise; setState((s) => ({ ...s, status: 'loading' })); promise.then( (result) => { // We dont want to handle result/error of non-latest function // this approach helps to avoid race conditions if (promise === promiseRef.current) { setState((s) => ({ ...s, status: 'success', error: undefined, result })); } }, (error: Error) => { // We dont want to handle result/error of non-latest function // this approach helps to avoid race conditions if (promise === promiseRef.current) { setState((s) => ({ ...s, status: 'error', error })); } } ); return promise; }, reset() { setState({ status: 'not-executed', error: undefined, result: initialValue, }); promiseRef.current = undefined; argsRef.current = undefined; }, }); return [ useMemo( () => ({ reset() { methods.current.reset(); }, execute: (...params: Args) => methods.current.execute(...params), }), // eslint-disable-next-line react-hooks/exhaustive-deps [] ), state, { promise: promiseRef.current, lastArgs: argsRef.current }, ]; } /** * Like `useRef`, but it returns immutable ref that contains actual value. * * @param value */ function useSyncedRef(value: T): { readonly current: T } { const ref = useRef(value); ref.current = value; return useMemo( () => Object.freeze({ get current() { return ref.current; }, }), [] ); } // --- utility functions to help with request state assertions --- export function isError(state: AsyncState): state is AsyncStateWithError { return state.status === 'error'; } export function isSuccess(state: AsyncState): state is AsyncStateFulfilled { return state.status === 'success'; } export function isUninitialized(state: AsyncState): state is AsyncStateUninitialized { return state.status === 'not-executed'; } export function isLoading(state: AsyncState): state is AsyncStateLoading { return state.status === 'loading'; } export function anyOfRequestState(...states: Array>) { return { uninitialized: states.every(isUninitialized), loading: states.some(isLoading), error: states.find(isError)?.error, success: states.some(isSuccess), }; } /** * This is only used for testing and serializing the async state */ export function SerializeState({ state }: { state: AsyncState }) { return ( <> {isUninitialized(state) && 'uninitialized'} {isLoading(state) && 'loading'} {isSuccess(state) && 'success'} {isSuccess(state) && `result: ${JSON.stringify(state.result, null, 2)}`} {isError(state) && `error: ${stringifyErrorLike(state.error)}`} ); }