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",