import { DependencyList, useState, useCallback, useEffect, useRef } from 'react'; export type UseAsyncProps = { /** The async callback or an array of async callbacks to call for fetching data. */ callback: (() => TReturnType | Promise) | Array<() => TReturnType | Promise>; /** The default value to populate the data as when initially mounted or reloading data. */ defaultValue: TReturnType | TDefaultValueType; /** Optional toggle to ignore previous async callback data */ ignorePrevious?: boolean; }; /** * Hook for invoking async code and keeping track of its state. * * Data is loaded in 3 different ways: * 1. On initial mount. * 2. When any of the `deps` change. * 3. When the `reload` function is called. */ export const useAsync = ( { callback, defaultValue, ignorePrevious = false }: UseAsyncProps, deps: DependencyList, ) => { const [data, setData] = useState(defaultValue); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const requestCountRef = useRef(0); const reload = useCallback(() => { setIsLoading(true); setError(null); setData(defaultValue); const currentRequestCount = ignorePrevious ? ++requestCountRef.current : 0; try { const isArrayOfCallbacks = Array.isArray(callback); const promises = isArrayOfCallbacks ? callback.map((cb) => cb()) : [callback()]; if (!(promises[0] instanceof Promise)) { const result = promises[0]; setData(result); setIsLoading(false); return; } Promise.all(promises) .then((resolved: TReturnType[]) => { // If ignorePrevious is set to TRUE we only set the new data if it's the most recent request // and not a previous request that has taken longer than expected. if (!ignorePrevious || currentRequestCount === requestCountRef.current) { setData(isArrayOfCallbacks ? (resolved as TReturnType) : resolved[0]); setIsLoading(false); } }) .catch((e: unknown) => { if (!ignorePrevious || currentRequestCount === requestCountRef.current) { setError(e instanceof Error ? e : new Error(String(e))); setIsLoading(false); } }); } catch (e: unknown) { setError(e instanceof Error ? e : new Error(String(e))); setIsLoading(false); } }, deps); // Reload data on dependency change (and initial mount) useEffect(() => { reload(); }, deps); return { data, error, isLoading, reload }; };