diff --git a/hooks/index.ts b/hooks/index.ts index 5e0f14c..e4dee43 100644 --- a/hooks/index.ts +++ b/hooks/index.ts @@ -4,3 +4,4 @@ export * from './useFocus'; export * from './useRetry'; export * from './useServiceEffect'; export * from './useOptionalDependency'; +export * from './useLazyLoadData'; diff --git a/hooks/useLazyLoadData/README.md b/hooks/useLazyLoadData/README.md new file mode 100644 index 0000000..91cbbfc --- /dev/null +++ b/hooks/useLazyLoadData/README.md @@ -0,0 +1,283 @@ +# `useLazyLoadData` + +_A React hook that optimizes promisable functions while abstracting away complexity with built-in lazy loading, caching, and dependency resolution, without needing to be invoked._ + +## Rationale +In larger applications, there are often core pieces of data required to render different aspects and features. Suppose, for instance, you are building a shopping application that has _wish list_ and _recommended items_ pages. Chances are, those pages would make _user wish list_ and _recommendations_ calls, respectively. However, in this example, both of these calls (and others) require the response of a _shopping profile_ call. + +A naive implimentation may be: + +```ts +import React from 'react'; +import {useLoadData} from '@optum/react-hooks'; + +// Wish List page +export const WishList = (props) => { + const loadedShoppingProfile = useLoadData(fetchShoppingProfile); + const loadedWishList = useLoadData((shoppingProfile) => { + return fetchWishList(shoppingProfile) + }, [loadedShoppingProfile]); + + ... +} + +// Shopping Recommendations page +export const ShoppingRecommendations = (props) => { + const loadedShoppingProfile = useLoadData(fetchShoppingProfile); + const loadedRecommendations = useLoadData((shoppingProfile) => { + return fetchRecommendations(shoppingProfile) + }, [loadedShoppingProfile]); + + ... +} +``` + +While this code would function, it would not be optimized. If a user were to navigate back and forth between the _wish list_ and _recommendations_ pages, we would notice `fetchShoppingProfile` invoked each and every time. This could be improved by leveraging the callbacks and intitial data arguments of `useLoadData` to cache and reuse responses. However, doing so relies on the developer remembering to implement/intregrate with the application's cache in addition to having a cohesive and coordinated cache already in place. + +A slightly more fool-proof and optimal way of handling this would be instantiating `loadedShoppingProfile` into a top level context available in both our pages: + +```ts +// Shopping Provider at root of application +export const ShoppingProvider = (props) => { + const loadedShoppingProfile = useLoadData(fetchShoppingProfile); + return ( + + {children} + + ); +} + +// Wish List page +export const WishList = (props) => { + const {loadedShoppingProfile} = useShoppingContext(); + const loadedWishList = useLoadData((shoppingProfile) => { + return fetchWishList(shoppingProfile) + }, [loadedShoppingProfile]); + + ... +} + +// Shopping Recommendations page +export const ShoppingRecommendations = (props) => { + const {loadedShoppingProfile} = useShoppingContext(); + const loadedRecommendations = useLoadData((shoppingProfile) => { + return fetchRecommendations(shoppingProfile) + }, [loadedShoppingProfile]); + + ... +} +``` + +Moving `loadedShoppingProfile` into a context eliminates the risk of fetching the _shopping profile_ more than once, while removing cognitive overhead for caching. However,`fetchShoppingProfile` will now _always_ be invoked, no matter which page the user lands on. For example, if the application's landing page does not require _shopping profile_, we would be needlessly fetching this data. While an argument could be made that this pattern "prefetches" data for other pages that require it, there is no guarantee a user would ever land on those pages, making this a needlessly costly operation for both front and backend. + +**Enter `useLazyLoadData`:** +This is where `useLazyLoadData` truly shines - it simultaneously improves perfomance with behind-the-scene caching and spares developers of that cognitive overhead, all while guaranteeing calls are only made _on demand_. + + +```ts +// Shopping Provider at root of application +export const ShoppingProvider = (props) => { + const lazyFetchShoppingProfile = useLazyLoadData(fetchShoppingProfile); + return ( + + {children} + + ); +} + +// Wish List page +export const WishList = (props) => { + const {lazyFetchShoppingProfile} = useShoppingContext(); + const loadedShoppingProfile = useLoadData(lazyFetchShoppingProfile); + const loadedWishList = useLoadData((shoppingProfile) => { + return fetchWishList(shoppingProfile) + }, [loadedShoppingProfile]); + + ... +} + +// Shopping Recommendations page +export const ShoppingRecommendations = (props) => { + const {lazyFetchShoppingProfile} = useShoppingContext(); + const loadedShoppingProfile = useLoadData(lazyFetchShoppingProfile); + const loadedRecommendations = useLoadData((shoppingProfile) => { + return fetchRecommendations(shoppingProfile) + }, [loadedShoppingProfile]); + + ... +} +``` +With this change, `fetchShoppingProfile` does not get invoked until the user lands on a page that requires it. Additionally, any subsequent page that invokes `lazyFetchShoppingProfile` will directly received the cached result (which is **not** a promise!) rather than making a new shopping profile call. + +--- + +## Usage +`useLazyLoadData` supports two overloads. Each overload handles a specific usecase, and will be covered seperately in this documentation: +### Overload 1: Basic usage +At bare minimum, `useLazyLoadData` takes in a promisable function, _fetchData_, as it's base parameter, and returns a wrapped version of this function. Call this _lazyFetchData_ for this exmaple. No matter how many quick successive calles to `lazyFetchData` are made, _fetchData_ only is invoked **once**: + +```ts +const lazyFetchData = useLazyLoadData(fetchData); + +const promise1 = lazyFetchData(); // intitializes a promise +const promise2 = lazyFetchData(); // reuses the same promise intialized in above line + +const [data1, data2] = await Promise.all([promise1, promise2]); + +const data3 = lazyFetchData(); // data will not be a promise, since the shared promise resolved and the now-cached returned instead! +``` + +#### Overriding Cache +By default, `useLazyLoadData` will try to either return cached data where available, or reuse a promise if one is currently active. However, sometimes it is necessary to be able to fetch fresh data. Here's how: + +```ts +const lazyFetchData = useLazyLoadData(fetchData); + + +// lazyFetchData will override cache when passed true +const freshData = await lazyFetchData(true); +``` + +#### Dependencies +Another strength of `useLazyLoadData` is it's dependency management. Similar to `useLoadData`, the hook takes an array of dependencies. Typically, these dependencies would be partial calls to other promisable functions (for instance, other functions given by `useLazyLoadData`). These dependencies, once resolved, are injected into the _fetchData_ function you passed as argument: + +```ts +const lazyFetchDependency = useLazyLoadData(fetchDependency); +const lazyFetchData = useLazyLoadData(([dep]) => { //dep will be the awaited return type of fetchDependancy + return fetchData(dep) +}, [lazyFetchDependency]); +const lazyFetchResult = useLazyLoadData(([dep, data]) => { + return fetchResult(dep, data) +}, [lazyFetchDependency, lazyFetchData]); +``` +In this example, both `fetchDependency` and `fetchData` will only be invoked once if `lazyFetchResult` is invoked. Notice how `useLazyLoadData` cleans up what could otherwise turn into messy _await_ statements, and effectively abstracts away complexity involved with making the actual call with how it handles dependencies. + + +#### Initial Data +Should you already have initial data (either from a cache or SSR), you can pass it into `useLazyLoadData`: + + +```ts +const lazyFetchNames = useLazyLoadData(fetchNumbers, [], [1,2,3,4]); + +lazyFetchNames(); // will return [1,2,3,4] + +lazyFetchNames(true); // will override cache and invoke fetchNumbers + +``` + +#### Callback +`useLazyLoadData` allows a callback to be passed. This function will only be invoked if the underlying _fetchData_ function passed successfully resolves, or when initial data is returned: + +```ts +const lazyFetchData = useLazyLoadData(fetchData, [], undefined, (res) => setCache(res)); + +``` + +### Overload 2: With Arguments +There may be times where you want to be able to pass arguments into the function `useLazyLoadData` exposes (apart from overriding cache). This can be achieved doing the following: + + +```ts +const lazyFetchDependency = useLazyLoadData(fetchDependency); + +/* +In this example, lazyFetchData accepts arg1 and arg2. +Arguments are passed after the resolved dependency array +*/ +const lazyFetchData = useLazyLoadData(([dep], arg1: string, arg2: number) => { + const something = doSomething(arg1, arg2) + return fetchData(dep, something) +}, +(arg1, arg2) => arg1, // to be discussed in the next section +[lazyFetchDependency] // dependencies still work as before +); + +lazyFetchData(false, 'argument', 2) // overrideCache always remains the first argument +``` + +#### Caching by arguments +When enabling arguments for the function `useLazyLoadData` exposes, useLazyLoadData assumes different arguments may yield different results. For this reason, useLazyLoadData will map promises and results (once available) to a serialized version of the arguments passed. As soon as your _fetchData_ function accepts arguments (apart from dependencies), `useLazyLoadData` requires passing a function telling it _how_ to cache with those args: + +```ts +const lazyFetchDependency = useLazyLoadData(fetchDependency); + +const lazyFetchData = useLazyLoadData(([dep], arg1: string, arg2: SomeObject) => { + const something = doSomething(arg1, arg2); + return fetchData(dep, something); + }, [lazyFetchDependency], undefined, undefined, + /* + arg1 and arg2 are the same as above. + In this example, we only wish to cache by a property of arg2 + */ + (arg1, arg2) => (arg2.identifier)); +``` + +#### Dependencies +Dependendies work the same in this overload as in the previous one. See above example for argument positioning. + +#### Initial Data +This overload still allows passing _initial data_. However, in this mode, responses are cached by arguments passed (see previous section). Therefore, only passing a response will not be very meaningful. For this reason, this overload's type for _initial data_ is a key-value mapping of responses, where each key is the serialized version of arguments expected to yield the value: + +```ts +const lazyFetchAge = useLazyLoadData(async ([], firstName: string, lastName: string) => { + return await fetchAge(firstName, lastName) +}, +(firstName, lastName) => ([firstName, lastName].join('-')), +[], +{ + 'John-Smith': 24, + 'Sarah-Jane': 42, + 'Joe-Jonson': 13 +}); + +lazyFetchAge(false, 'John', 'Smith') // fetchAge will not be invoked, as it maps to a provided initial data field +lazyFetchAge(false, 'Sarah', 'Smith') // fetchAge will be invoked, since initial data did not contain key for 'Sarah-Smith' +lazyFetchAge(true, 'John', 'Smith') // fetchAge will be invoked, since cache is overriden +``` + +#### Callback +The callback in this overload only varies slightly from the other overload. +In addition to the callback function being passed the result, it will also receive the original arguments used to retrieve said result for convenience: + +```ts +const lazyFetchAge = useLazyLoadData(async ([], firstName: string, lastName: string) => { + return await fetchAge(firstName, lastName) +}, +(firstName, lastName) => ([firstName, lastName].join('-')), +[], +undefined, +(res, firstName, lastName) => { + console.log(`The age of ${firstName} ${lastName} is ${res}`) +}); +``` + +## API +`useLazyLoadData` takes the following arguments: + +### Arguments +#### Overload 1: Basic Usage + +| Name | Type | Description | +|-|-|-| +| `fetchData` | `([...AwaitedReturnType]) => Promisable` | The function to be invoked with resolved dependencies| +| `deps` | `[...deps]` _(optional)_ | Dependencies to be invoked and/or resolved before injecting into and invoking `fetchData`. These may typically be other instances of `useLazyLoadData`. | +| `initialData` | `T` _(optional)_ | If passed, function will return initial data when no additional arguments are passed. | +| `callback` | `(data: T) => void` _(optional)_ | Gets invoked with the result of `fetchData` after resolving. | + + + +#### Overload 2: With arguments + +| Name | Type | Description | +|-|-|-| +| `fetchData` | `([...deps], ...args) => Promisable`| The function to be invoked with dependencies and arguments| +| `getCacheKey` | `(...args) => string` | Function declaring how promises/results are mapped by arguments passed into returned function. | +| `deps` | `[...deps]` _(optional)_ | Dependencies to be invoked and/or resolved before injecting into and invoking `fetchData`. These may typically be other instances of `useLazyLoadData`. | +| `initialData` | `Record` _(optional)_ | If passed, function will return initial data when no additional arguments are passed. | +| `callback` | `(data: T, ...args) => void` _(optional)_ | Gets invoked with the result of `fetchData` after resolving. | + +### Return Type +The return value of `useLazyLoadData` is `(overrideCache?: boolean, ...args: Args) => Promisable`, where `T` is the type of the result of `fetchData` (`Args` will be `Never` over Overload 1) + + diff --git a/hooks/useLazyLoadData/index.ts b/hooks/useLazyLoadData/index.ts new file mode 100644 index 0000000..79cd62f --- /dev/null +++ b/hooks/useLazyLoadData/index.ts @@ -0,0 +1 @@ +export * from './useLazyLoadData'; diff --git a/hooks/useLazyLoadData/useLazyLoadData.test.ts b/hooks/useLazyLoadData/useLazyLoadData.test.ts new file mode 100644 index 0000000..df0e20d --- /dev/null +++ b/hooks/useLazyLoadData/useLazyLoadData.test.ts @@ -0,0 +1,205 @@ +import {renderHook} from '@testing-library/react'; +import {expect, describe} from '@jest/globals'; + +import {useLazyLoadData} from './useLazyLoadData'; + +const fetchData = jest.fn(async () => Promise.resolve('result')); +const fetchDep = jest.fn(async () => Promise.resolve('dep')); +const callback = jest.fn(); + +describe('useLazyLoadData', () => { + it('should only invoke fetchData when returned function is invoked', async () => { + const renderedHook = renderHook(() => useLazyLoadData(fetchData)); + const lazyFetchData = renderedHook.result.current; + expect(fetchData).toHaveBeenCalledTimes(0); + await lazyFetchData(); + expect(fetchData).toHaveBeenCalledTimes(1); + }); + + it('should invoke fetchData once when passed no args', async () => { + const renderedHook = renderHook(() => useLazyLoadData(fetchData)); + const lazyFetchData = renderedHook.result.current; + const res1 = await lazyFetchData(); + const res2 = await lazyFetchData(); + + expect(res1).toBe('result'); + expect(res2).toBe('result'); + expect(fetchData).toHaveBeenCalledTimes(1); + }); + + it('should distribute the same promise to immediate subsequent calls', async () => { + const renderedHook = renderHook(() => useLazyLoadData(fetchData)); + const lazyFetchData = renderedHook.result.current; + + const promise1 = lazyFetchData(); + const promise2 = lazyFetchData(); + + const res1 = await promise1; + const res2 = await promise2; + + expect(res1).toBe('result'); + expect(res2).toBe('result'); + + expect(fetchData).toHaveBeenCalledTimes(1); + }); + + it('should return non-promise after having already resolved fetchData', async () => { + const renderedHook = renderHook(() => useLazyLoadData(fetchData)); + const lazyFetchData = renderedHook.result.current; + const promise1 = lazyFetchData(); + expect(promise1).toBeInstanceOf(Promise); + await promise1; + + const res2 = lazyFetchData(); + expect(res2).not.toBeInstanceOf(Promise); + expect(res2).toBe('result'); + expect(fetchData).toHaveBeenCalledTimes(1); + }); + + it('should re-invoke fetchData when cache is overridden', async () => { + const renderedHook = renderHook(() => useLazyLoadData(fetchData)); + const lazyFetchData = renderedHook.result.current; + const res1 = await lazyFetchData(); + const res2 = await lazyFetchData(true); + + expect(res1).toBe('result'); + expect(res2).toBe('result'); + expect(fetchData).toHaveBeenCalledTimes(2); + }); + + it('should not invoke fetchData when passed initialData', () => { + const renderedHook = renderHook(() => useLazyLoadData(fetchData, [], 'cache')); + const lazyFetchData = renderedHook.result.current; + + const res1 = lazyFetchData(); + expect(res1).not.toBeInstanceOf(Promise); + expect(res1).toBe('cache'); + expect(fetchData).not.toHaveBeenCalled(); + }); + + it('should pass result of dependency and args into fetchData', async () => { + const renderedHook = renderHook(() => useLazyLoadData(fetchData, [fetchDep])); + const lazyFetchData = renderedHook.result.current as any; + + await lazyFetchData(false, 'arg'); + expect(fetchDep).toHaveBeenCalledTimes(1); + expect(fetchData).toHaveBeenCalledTimes(1); + expect(fetchData).toHaveBeenCalledWith(['dep'], 'arg'); + }); + + it('should re-invoke fetchData once for each time a different arg is passed', async () => { + const renderedHook = renderHook(() => useLazyLoadData(fetchData, ((arg: any) => arg) as any)); + const lazyFetchData = renderedHook.result.current as any; + const res1arg1 = await lazyFetchData(false, 'arg1'); + const res2arg1 = lazyFetchData(false, 'arg1'); + expect(res2arg1).not.toBeInstanceOf(Promise); + expect(res1arg1).toBe('result'); + expect(fetchData).toHaveBeenCalledTimes(1); + expect(fetchData).toHaveBeenCalledWith([], 'arg1'); + + const res1arg2 = await lazyFetchData(false, 'arg2'); + const res2arg2 = lazyFetchData(false, 'arg2'); + + expect(res2arg2).not.toBeInstanceOf(Promise); + expect(res1arg2).toBe('result'); + expect(fetchData).toHaveBeenCalledTimes(2); + expect(fetchData).toHaveBeenCalledWith([], 'arg2'); + + const res3arg1 = lazyFetchData(false, 'arg1'); + expect(res3arg1).not.toBeInstanceOf(Promise); + expect(fetchData).toHaveBeenCalledTimes(2); + }); + + it('should invoke callback with result data upon fetchData resolving', async () => { + const renderedHook = renderHook(() => useLazyLoadData(fetchData, [], undefined, callback)); + const lazyFetchData = renderedHook.result.current as any; + await lazyFetchData(); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith('result'); + }); + + it('should invoke callback with result data and provided args upon fetchData resolving', async () => { + const renderedHook = renderHook(() => useLazyLoadData(fetchData, [], undefined, callback)); + const lazyFetchData = renderedHook.result.current as any; + await lazyFetchData(false, 'arg1', 'arg2'); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith('result', 'arg1', 'arg2'); + }); + + it('should utilize provided cacheMap when applicable', async () => { + const initialData = { + arg1: 'res1', + arg2: 'res2' + }; + + const renderedHook = renderHook(() => + useLazyLoadData(fetchData, ((arg: any) => arg) as any, [], initialData, callback) + ); + const lazyFetchData = renderedHook.result.current as any; + const res1 = lazyFetchData(false, 'arg1'); + const res2 = lazyFetchData(false, 'arg2'); + expect(res1).not.toBeInstanceOf(Promise); + expect(res1).toBe('res1'); + + expect(res2).not.toBeInstanceOf(Promise); + expect(res2).toBe('res2'); + expect(fetchData).toHaveBeenCalledTimes(0); + + const res3 = lazyFetchData(false, 'arg3'); + expect(res3).toBeInstanceOf(Promise); + + await expect(res3).resolves.toBe('result'); + + const cachedRes3 = lazyFetchData(false, 'arg3'); + expect(cachedRes3).not.toBeInstanceOf(Promise); + expect(cachedRes3).toBe('result'); + expect(fetchData).toHaveBeenCalledTimes(1); + }); + + it('should not reuse error promises', async () => { + const getFailThenSuccess = jest + .fn() + .mockImplementationOnce(async () => Promise.reject(Error())) + .mockImplementationOnce(async () => Promise.resolve('result')); + + const renderedHook = renderHook(() => useLazyLoadData(getFailThenSuccess)); + const lazyFetchData = renderedHook.result.current as any; + + let result; + + try { + result = await lazyFetchData(); + } catch (error) { + result = 'error'; + } + + expect(result).toBe('error'); + expect(getFailThenSuccess).toHaveBeenCalledTimes(1); + + result = await lazyFetchData(); + + expect(result).toBe('result'); + expect(getFailThenSuccess).toHaveBeenCalledTimes(2); + }); + + it('should invoke callback any time returned data changes', async () => { + const renderedHook = renderHook(() => useLazyLoadData(fetchData, [], 'data', callback)); + const lazyFetchData = renderedHook.result.current as any; + await lazyFetchData(); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith('data'); + + lazyFetchData(); + expect(callback).toHaveBeenCalledTimes(1); + + await lazyFetchData(true); + expect(callback).toHaveBeenCalledTimes(2); + expect(callback).toHaveBeenCalledWith('result'); + + lazyFetchData(); + expect(callback).toHaveBeenCalledTimes(2); + }); +}); diff --git a/hooks/useLazyLoadData/useLazyLoadData.ts b/hooks/useLazyLoadData/useLazyLoadData.ts new file mode 100644 index 0000000..bf96361 --- /dev/null +++ b/hooks/useLazyLoadData/useLazyLoadData.ts @@ -0,0 +1,141 @@ +import {Promisable} from '../../types'; +import {useRef} from 'react'; + +type InvokedDeps = { + // eslint-disable-next-line @typescript-eslint/ban-types + [K in keyof T]: T[K] extends Function ? ReturnType : T[K]; +}; +type ResolvedDeps = { + // eslint-disable-next-line @typescript-eslint/ban-types + [K in keyof T]: T[K] extends Function ? Awaited> : T[K]; +}; + +type ExcludeFirst = T extends [any, ...infer Rest] ? Rest : never; + +interface NormalizeArgumentWithArgs Promisable, T, Deps extends any[]> { + getCacheKey: (...args: ExcludeFirst>) => string; + deps?: readonly [...Deps]; + initialDataMap?: Record; + callback?: (data: T, ...args: ExcludeFirst>) => void; + initialData: undefined; +} + +interface NormalizeArgumentWithoutArgs { + deps?: readonly [...Deps]; + initialData?: T; + callback?: (data: T) => void; + getCacheKey: undefined; + initialDataMap: undefined; +} + +type NormalizeArgumentOverloads Promisable, T, Deps extends any[]> = + | NormalizeArgumentWithArgs + | NormalizeArgumentWithoutArgs; + +function normalizeArgumentOverloads Promisable, T, Deps extends any[]>( + arg2?: unknown, + arg3?: unknown, + arg4?: unknown, + arg5?: unknown +): NormalizeArgumentOverloads { + if (typeof arg2 === 'function') { + return { + getCacheKey: arg2, + deps: arg3, + initialDataMap: arg4, + callback: arg5 + } as NormalizeArgumentWithArgs; + } + + return { + deps: arg2, + initialData: arg3, + callback: arg4 + } as NormalizeArgumentWithoutArgs; +} + +// first overload for NO args +export function useLazyLoadData( + fetchData: (deps: readonly [...ResolvedDeps]) => Promisable, + deps?: readonly [...Deps], + initialData?: T, + callback?: (data: T) => void +): (disableCache?: boolean) => Promisable; + +// second overload WITH args +export function useLazyLoadData( + fetchData: (deps: readonly [...ResolvedDeps], ...args: readonly [...Args]) => Promisable, + getCacheKey: (...args: ExcludeFirst>) => string, + deps?: readonly [...Deps], + initialData?: Record, + callback?: (data: T, ...args: ExcludeFirst>) => void +): (disableCache?: boolean, ...args: readonly [...Args]) => Promisable; + +export function useLazyLoadData( + fetchData: (deps: readonly [...ResolvedDeps], ...args: readonly [...Args]) => Promisable, + arg2?: unknown, + arg3?: unknown, + arg4?: unknown, + arg5?: unknown +): (disableCache?: boolean, ...args: readonly [...Args]) => Promisable { + const { + deps, + callback, + getCacheKey = () => 'default', + initialData, + initialDataMap + } = normalizeArgumentOverloads(arg2, arg3, arg4, arg5); + /* + Tracks whether data yielded from set of args as already been returned. + Used to determine whether or not initialData needs to be passed into callback + */ + const returnIndicators = useRef>({}); + const cache = useRef>(initialDataMap || {default: initialData}); + const promiseSingleton = useRef>>({}); + // eslint-disable-next-line @typescript-eslint/promise-function-async + return (disableCache = false, ...args) => { + const key = getCacheKey(...(args as unknown as ExcludeFirst>)); + const cachedData = cache.current[key]; + const relevantPromise = promiseSingleton.current[key]; + const invokeCallback = !returnIndicators.current[key]; + returnIndicators.current[key] = true; + + async function handleFetchData() { + const promisedDeps = deps?.map((dep: unknown) => { + return typeof dep === 'function' ? (dep() as unknown) : dep; + }) as InvokedDeps; + + const promisedData = Promise.all(promisedDeps || []).then(async (resolvedDeps) => { + return fetchData(resolvedDeps, ...args); + }); + promiseSingleton.current[key] = promisedData; + let data: T | undefined; + try { + data = await promisedData; + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete promiseSingleton.current[key]; + throw error; + } + cache.current[key] = data; + callback?.(data, ...(args as unknown as ExcludeFirst>)); + return data; + } + + if (disableCache) { + return handleFetchData(); + } + if (cachedData !== undefined && invokeCallback) { + callback?.(cachedData, ...(args as unknown as ExcludeFirst>)); + return cachedData; + } + if (cachedData !== undefined) { + return cachedData; + } + if (relevantPromise) { + return relevantPromise; + } + + return handleFetchData(); + }; +} diff --git a/package.json b/package.json index 9540ff2..c6ac35f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@optum/react-hooks", - "version": "1.0.5", + "version": "1.1.0-next.0", "description": "A reusable set of React hooks", "repository": "https://github.com/Optum/react-hooks", "license": "Apache 2.0",