diff --git a/hooks/useLoadData/useLoadData.test.ts b/hooks/useLoadData/useLoadData.test.ts index a8dd703..92779ed 100644 --- a/hooks/useLoadData/useLoadData.test.ts +++ b/hooks/useLoadData/useLoadData.test.ts @@ -370,4 +370,17 @@ describe('useLoadData', () => { expect(getFail).toHaveBeenCalledTimes(2); expect(mockRetry).toHaveBeenCalledTimes(0); }); + + it('should set isError to true if the fetch data function throws a non-promise exception', () => { + const {result} = renderHook(() => { + return useLoadData(() => { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw 'immediate failure'; + }); + }); + + expect(result.current.isInProgress).toBe(false); + expect(result.current.isError).toBe(true); + expect(result.current.error).toBe('immediate failure'); + }); }); diff --git a/hooks/useLoadData/useLoadData.ts b/hooks/useLoadData/useLoadData.ts index 83a2f6a..ed4a8a7 100644 --- a/hooks/useLoadData/useLoadData.ts +++ b/hooks/useLoadData/useLoadData.ts @@ -163,29 +163,56 @@ export function useLoadData( const initialPromise = useMemo(() => { const correctedArgs = correctOptionalDependencies(fetchDataArgs); if (!data && counter < 1 && checkArgsAreLoaded(correctedArgs)) { - return fetchData(...((correctedArgs.map(unboxApiResponse) || []) as Parameters)); + try { + return { + res: fetchData(...((correctedArgs.map(unboxApiResponse) || []) as Parameters)), + error: undefined + }; + } catch (e) { + return { + res: undefined, + error: e + }; + } } else { - return undefined; + return {res: undefined, error: undefined}; } }, [counter]); - const nonPromiseResult = initialPromise instanceof Promise ? undefined : initialPromise; + const nonPromiseResult = initialPromise.res instanceof Promise ? undefined : initialPromise.res; const initialData = data || nonPromiseResult; + // Initialize our pending data to one of three possible states: + // 1. If initial data was supplied or if the fetchData function returned a non-Promise value, + // then our initial state will be already "resolved" (not in-progress and not error, we already have the result) + // 2. If initial data was not supplied and fetchData returned a Promise, then our initial state is in-progress + // 3. If initial data was not supplied and fetchData threw a *synchronous* (non-Promise) exception, + // then our initial state is "rejected" (not in-progress and already has an error value) + const initialDataResolved = + initialData && + ({ + isInProgress: false, + isError: false, + result: initialData, + error: undefined + } as const); + const initialDataRejected = + initialPromise.error !== undefined && + ({ + isInProgress: false, + isError: true, + result: undefined, + error: initialPromise.error + } as const); + const initialDataPending = { + isInProgress: true, + isError: false, + result: undefined, + error: undefined + } as const; + const [pendingData, setPendingData] = useState>( - initialData - ? { - isInProgress: false, - isError: false, - result: initialData, - error: undefined - } - : { - isInProgress: true, - isError: false, - result: undefined, - error: undefined - } + initialDataResolved || initialDataRejected || initialDataPending ); function retry() { @@ -224,9 +251,9 @@ export function useLoadData( const unboxedArgs = correctedArgs.map(unboxApiResponse); const fetchedData = - initialPromise === undefined + initialPromise.res === undefined ? await fetchData(...((unboxedArgs || []) as Parameters)) - : await initialPromise; + : await initialPromise.res; setPendingData({ isInProgress: false,