Skip to content

Commit 355e40e

Browse files
feat: useLazyLoadData
1 parent c741dc0 commit 355e40e

File tree

1 file changed

+141
-0
lines changed

1 file changed

+141
-0
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import {Promisable} from '@optum/react-hooks';
2+
import {useRef} from 'react';
3+
4+
type InvokedDeps<T extends any[]> = {
5+
// eslint-disable-next-line @typescript-eslint/ban-types
6+
[K in keyof T]: T[K] extends Function ? ReturnType<T[K]> : T[K];
7+
};
8+
type ResolvedDeps<T extends any[]> = {
9+
// eslint-disable-next-line @typescript-eslint/ban-types
10+
[K in keyof T]: T[K] extends Function ? Awaited<ReturnType<T[K]>> : T[K];
11+
};
12+
13+
type ExcludeFirst<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;
14+
15+
interface NormalizeArgumentWithArgs<FetchData extends (...args: any[]) => Promisable<any>, T, Deps extends any[]> {
16+
getCacheKey: (...args: ExcludeFirst<Parameters<FetchData>>) => string;
17+
deps?: readonly [...Deps];
18+
initialDataMap?: Record<string, T | undefined>;
19+
callback?: (data: T, ...args: ExcludeFirst<Parameters<FetchData>>) => void;
20+
initialData: undefined;
21+
}
22+
23+
interface NormalizeArgumentWithoutArgs<T, Deps extends any[]> {
24+
deps?: readonly [...Deps];
25+
initialData?: T;
26+
callback?: (data: T) => void;
27+
getCacheKey: undefined;
28+
initialDataMap: undefined;
29+
}
30+
31+
type NormalizeArgumentOverloads<FetchData extends (...args: any[]) => Promisable<any>, T, Deps extends any[]> =
32+
| NormalizeArgumentWithArgs<FetchData, T, Deps>
33+
| NormalizeArgumentWithoutArgs<T, Deps>;
34+
35+
function normalizeArgumentOverloads<FetchData extends (...args: any[]) => Promisable<any>, T, Deps extends any[]>(
36+
arg2?: unknown,
37+
arg3?: unknown,
38+
arg4?: unknown,
39+
arg5?: unknown
40+
): NormalizeArgumentOverloads<FetchData, T, Deps> {
41+
if (typeof arg2 === 'function') {
42+
return {
43+
getCacheKey: arg2,
44+
deps: arg3,
45+
initialDataMap: arg4,
46+
callback: arg5
47+
} as NormalizeArgumentWithArgs<FetchData, T, Deps>;
48+
}
49+
50+
return {
51+
deps: arg2,
52+
initialData: arg3,
53+
callback: arg4
54+
} as NormalizeArgumentWithoutArgs<T, Deps>;
55+
}
56+
57+
// first overload for NO args
58+
export function useLazyLoadData<T, Deps extends any[]>(
59+
fetchData: (deps: readonly [...ResolvedDeps<Deps>]) => Promisable<T>,
60+
deps?: readonly [...Deps],
61+
initialData?: T,
62+
callback?: (data: T) => void
63+
): (disableCache?: boolean) => Promisable<T>;
64+
65+
// second overload WITH args
66+
export function useLazyLoadData<Args extends any[], T, Deps extends any[]>(
67+
fetchData: (deps: readonly [...ResolvedDeps<Deps>], ...args: readonly [...Args]) => Promisable<T>,
68+
getCacheKey: (...args: ExcludeFirst<Parameters<typeof fetchData>>) => string,
69+
deps?: readonly [...Deps],
70+
initialData?: Record<string, T | undefined>,
71+
callback?: (data: T, ...args: ExcludeFirst<Parameters<typeof fetchData>>) => void
72+
): (disableCache?: boolean, ...args: readonly [...Args]) => Promisable<T>;
73+
74+
export function useLazyLoadData<Args extends any[], T, Deps extends any[]>(
75+
fetchData: (deps: readonly [...ResolvedDeps<Deps>], ...args: readonly [...Args]) => Promisable<T>,
76+
arg2?: unknown,
77+
arg3?: unknown,
78+
arg4?: unknown,
79+
arg5?: unknown
80+
): (disableCache?: boolean, ...args: readonly [...Args]) => Promisable<T> {
81+
const {
82+
deps,
83+
callback,
84+
getCacheKey = () => 'default',
85+
initialData,
86+
initialDataMap
87+
} = normalizeArgumentOverloads<typeof fetchData, T, Deps>(arg2, arg3, arg4, arg5);
88+
/*
89+
Tracks whether data yielded from set of args as already been returned.
90+
Used to determine whether or not initialData needs to be passed into callback
91+
*/
92+
const returnIndicators = useRef<Record<string, boolean>>({});
93+
const cache = useRef<Record<string, T | undefined>>(initialDataMap || {default: initialData});
94+
const promiseSingleton = useRef<Record<string, Promisable<T>>>({});
95+
// eslint-disable-next-line @typescript-eslint/promise-function-async
96+
return (disableCache = false, ...args) => {
97+
const key = getCacheKey(...(args as unknown as ExcludeFirst<Parameters<typeof fetchData>>));
98+
const cachedData = cache.current[key];
99+
const relevantPromise = promiseSingleton.current[key];
100+
const invokeCallback = !returnIndicators.current[key];
101+
returnIndicators.current[key] = true;
102+
103+
async function handleFetchData() {
104+
const promisedDeps = deps?.map((dep: unknown) => {
105+
return typeof dep === 'function' ? (dep() as unknown) : dep;
106+
}) as InvokedDeps<Deps>;
107+
108+
const promisedData = Promise.all(promisedDeps || []).then(async (resolvedDeps) => {
109+
return fetchData(resolvedDeps, ...args);
110+
});
111+
promiseSingleton.current[key] = promisedData;
112+
let data: T | undefined;
113+
try {
114+
data = await promisedData;
115+
} catch (error) {
116+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
117+
delete promiseSingleton.current[key];
118+
throw error;
119+
}
120+
cache.current[key] = data;
121+
callback?.(data, ...(args as unknown as ExcludeFirst<Parameters<typeof fetchData>>));
122+
return data;
123+
}
124+
125+
if (disableCache) {
126+
return handleFetchData();
127+
}
128+
if (cachedData !== undefined && invokeCallback) {
129+
callback?.(cachedData, ...(args as unknown as ExcludeFirst<Parameters<typeof fetchData>>));
130+
return cachedData;
131+
}
132+
if (cachedData !== undefined) {
133+
return cachedData;
134+
}
135+
if (relevantPromise) {
136+
return relevantPromise;
137+
}
138+
139+
return handleFetchData();
140+
};
141+
}

0 commit comments

Comments
 (0)