@croct/plug-react 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -839
- package/hooks/useContent.d.ts +1 -0
- package/hooks/useContent.js +16 -3
- package/hooks/useContent.js.map +1 -1
- package/hooks/useEvaluation.d.ts +1 -0
- package/hooks/useEvaluation.js +25 -12
- package/hooks/useEvaluation.js.map +1 -1
- package/hooks/useLoader.js +41 -18
- package/hooks/useLoader.js.map +1 -1
- package/package.json +3 -11
- package/src/hooks/useContent.test.ts +118 -7
- package/src/hooks/useContent.ts +31 -3
- package/src/hooks/useEvaluation.test.ts +88 -2
- package/src/hooks/useEvaluation.ts +43 -15
- package/src/hooks/useLoader.test.ts +91 -4
- package/src/hooks/useLoader.ts +55 -21
|
@@ -1,27 +1,17 @@
|
|
|
1
1
|
import {JsonValue} from '@croct/plug/sdk/json';
|
|
2
2
|
import {EvaluationOptions} from '@croct/sdk/facade/evaluatorFacade';
|
|
3
|
+
import {useEffect, useState} from 'react';
|
|
3
4
|
import {useLoader} from './useLoader';
|
|
4
5
|
import {useCroct} from './useCroct';
|
|
5
6
|
import {isSsr} from '../ssr-polyfills';
|
|
6
7
|
import {hash} from '../hash';
|
|
7
8
|
|
|
8
|
-
function cleanEvaluationOptions(options: EvaluationOptions): EvaluationOptions {
|
|
9
|
-
const result: EvaluationOptions = {};
|
|
10
|
-
|
|
11
|
-
for (const [key, value] of Object.entries(options) as Array<[keyof EvaluationOptions, any]>) {
|
|
12
|
-
if (value !== undefined) {
|
|
13
|
-
result[key] = value;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return result;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
9
|
export type UseEvaluationOptions<I, F> = EvaluationOptions & {
|
|
21
10
|
initial?: I,
|
|
22
11
|
fallback?: F,
|
|
23
12
|
cacheKey?: string,
|
|
24
13
|
expiration?: number,
|
|
14
|
+
staleWhileLoading?: boolean,
|
|
25
15
|
};
|
|
26
16
|
|
|
27
17
|
type UseEvaluationHook = <T extends JsonValue, I = T, F = T>(
|
|
@@ -33,20 +23,58 @@ function useCsrEvaluation<T = JsonValue, I = T, F = T>(
|
|
|
33
23
|
query: string,
|
|
34
24
|
options: UseEvaluationOptions<I, F> = {},
|
|
35
25
|
): T | I | F {
|
|
36
|
-
const {
|
|
26
|
+
const {
|
|
27
|
+
cacheKey,
|
|
28
|
+
fallback,
|
|
29
|
+
expiration,
|
|
30
|
+
staleWhileLoading = false,
|
|
31
|
+
initial: initialValue,
|
|
32
|
+
...evaluationOptions
|
|
33
|
+
} = options;
|
|
34
|
+
|
|
35
|
+
const [initial, setInitial] = useState<T | I | F | undefined>(initialValue);
|
|
37
36
|
const croct = useCroct();
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
const result = useLoader<T | I | F>({
|
|
40
39
|
cacheKey: hash(
|
|
41
40
|
`useEvaluation:${cacheKey ?? ''}`
|
|
42
41
|
+ `:${query}`
|
|
43
|
-
+ `:${JSON.stringify(options.attributes ??
|
|
42
|
+
+ `:${JSON.stringify(options.attributes ?? {})}`,
|
|
44
43
|
),
|
|
45
44
|
loader: () => croct.evaluate<T & JsonValue>(query, cleanEvaluationOptions(evaluationOptions)),
|
|
46
45
|
initial: initial,
|
|
47
46
|
fallback: fallback,
|
|
48
47
|
expiration: expiration,
|
|
49
48
|
});
|
|
49
|
+
|
|
50
|
+
useEffect(
|
|
51
|
+
() => {
|
|
52
|
+
if (staleWhileLoading) {
|
|
53
|
+
setInitial(current => {
|
|
54
|
+
if (current !== result) {
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return current;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
[result, staleWhileLoading],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cleanEvaluationOptions(options: EvaluationOptions): EvaluationOptions {
|
|
69
|
+
const result: EvaluationOptions = {};
|
|
70
|
+
|
|
71
|
+
for (const [key, value] of Object.entries(options) as Array<[keyof EvaluationOptions, any]>) {
|
|
72
|
+
if (value !== undefined) {
|
|
73
|
+
result[key] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
50
78
|
}
|
|
51
79
|
|
|
52
80
|
function useSsrEvaluation<T = JsonValue, I = T, F = T>(
|
|
@@ -16,11 +16,8 @@ describe('useLoader', () => {
|
|
|
16
16
|
|
|
17
17
|
beforeEach(() => {
|
|
18
18
|
cacheKey.next();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
jest.clearAllTimers();
|
|
23
19
|
jest.resetAllMocks();
|
|
20
|
+
jest.clearAllTimers();
|
|
24
21
|
});
|
|
25
22
|
|
|
26
23
|
// Needed to use fake timers and promises:
|
|
@@ -212,6 +209,96 @@ describe('useLoader', () => {
|
|
|
212
209
|
expect(loader).toHaveBeenCalledTimes(1);
|
|
213
210
|
});
|
|
214
211
|
|
|
212
|
+
it('should reload the value when the cache key changes without initial value', async () => {
|
|
213
|
+
jest.useFakeTimers();
|
|
214
|
+
|
|
215
|
+
const loader = jest.fn()
|
|
216
|
+
.mockResolvedValueOnce('foo')
|
|
217
|
+
.mockImplementationOnce(
|
|
218
|
+
() => new Promise(resolve => {
|
|
219
|
+
setTimeout(() => resolve('bar'), 10);
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const {result, rerender} = renderHook<any, {initial: string}>(
|
|
224
|
+
props => useLoader({
|
|
225
|
+
cacheKey: cacheKey.current(),
|
|
226
|
+
loader: loader,
|
|
227
|
+
initial: props?.initial,
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
await act(flushPromises);
|
|
232
|
+
|
|
233
|
+
rerender();
|
|
234
|
+
|
|
235
|
+
await waitFor(() => expect(result.current).toBe('foo'));
|
|
236
|
+
|
|
237
|
+
expect(loader).toHaveBeenCalledTimes(1);
|
|
238
|
+
|
|
239
|
+
cacheKey.next();
|
|
240
|
+
|
|
241
|
+
rerender({initial: 'loading'});
|
|
242
|
+
|
|
243
|
+
await waitFor(() => expect(result.current).toBe('loading'));
|
|
244
|
+
|
|
245
|
+
jest.advanceTimersByTime(10);
|
|
246
|
+
|
|
247
|
+
await waitFor(() => expect(result.current).toBe('bar'));
|
|
248
|
+
|
|
249
|
+
expect(loader).toHaveBeenCalledTimes(2);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should reload the value when the cache key changes with initial value', async () => {
|
|
253
|
+
jest.useFakeTimers();
|
|
254
|
+
|
|
255
|
+
const loader = jest.fn()
|
|
256
|
+
.mockImplementationOnce(
|
|
257
|
+
() => new Promise(resolve => {
|
|
258
|
+
setTimeout(() => resolve('foo'), 10);
|
|
259
|
+
}),
|
|
260
|
+
)
|
|
261
|
+
.mockImplementationOnce(
|
|
262
|
+
() => new Promise(resolve => {
|
|
263
|
+
setTimeout(() => resolve('bar'), 10);
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const {result, rerender} = renderHook<any, {initial: string}>(
|
|
268
|
+
props => useLoader({
|
|
269
|
+
cacheKey: cacheKey.current(),
|
|
270
|
+
initial: props?.initial ?? 'first content',
|
|
271
|
+
loader: loader,
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
await act(flushPromises);
|
|
276
|
+
|
|
277
|
+
expect(result.current).toBe('first content');
|
|
278
|
+
|
|
279
|
+
jest.advanceTimersByTime(10);
|
|
280
|
+
|
|
281
|
+
await act(flushPromises);
|
|
282
|
+
|
|
283
|
+
await waitFor(() => expect(result.current).toBe('foo'));
|
|
284
|
+
|
|
285
|
+
expect(loader).toHaveBeenCalledTimes(1);
|
|
286
|
+
|
|
287
|
+
cacheKey.next();
|
|
288
|
+
|
|
289
|
+
rerender({initial: 'second content'});
|
|
290
|
+
|
|
291
|
+
await waitFor(() => expect(result.current).toBe('second content'));
|
|
292
|
+
|
|
293
|
+
jest.advanceTimersByTime(10);
|
|
294
|
+
|
|
295
|
+
await act(flushPromises);
|
|
296
|
+
|
|
297
|
+
await waitFor(() => expect(result.current).toBe('bar'));
|
|
298
|
+
|
|
299
|
+
expect(loader).toHaveBeenCalledTimes(2);
|
|
300
|
+
});
|
|
301
|
+
|
|
215
302
|
it.each<[number, number|undefined]>(
|
|
216
303
|
[
|
|
217
304
|
// [Expected elapsed time, Expiration]
|
package/src/hooks/useLoader.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {useEffect, useRef, useState} from 'react';
|
|
1
|
+
import {useCallback, useEffect, useRef, useState} from 'react';
|
|
2
2
|
import {Cache, EntryOptions} from './Cache';
|
|
3
3
|
|
|
4
4
|
const cache = new Cache(60 * 1000);
|
|
@@ -8,38 +8,60 @@ export type CacheOptions<R> = EntryOptions<R> & {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export function useLoader<R>({initial, ...options}: CacheOptions<R>): R {
|
|
11
|
-
const
|
|
11
|
+
const {cacheKey} = options;
|
|
12
|
+
const loadedValue: R|undefined = cache.get<R>(cacheKey)?.result;
|
|
12
13
|
const [value, setValue] = useState(loadedValue !== undefined ? loadedValue : initial);
|
|
13
14
|
const mountedRef = useRef(true);
|
|
14
|
-
const
|
|
15
|
+
const initialRef = useRef(initial);
|
|
16
|
+
const previousCacheKey = useRef(cacheKey);
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
if (mountedRef.current) {
|
|
25
|
-
setValue(resolvedValue);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
return;
|
|
18
|
+
const load = useStableCallback(() => {
|
|
19
|
+
try {
|
|
20
|
+
setValue(cache.load(options));
|
|
21
|
+
} catch (result: unknown) {
|
|
22
|
+
if (result instanceof Promise) {
|
|
23
|
+
result.then((resolvedValue: R) => {
|
|
24
|
+
if (mountedRef.current) {
|
|
25
|
+
setValue(resolvedValue);
|
|
30
26
|
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setValue(undefined);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
const reset = useStableCallback(() => {
|
|
37
|
+
const newLoadedValue: R|undefined = cache.get<R>(cacheKey)?.result;
|
|
38
|
+
|
|
39
|
+
setValue(newLoadedValue !== undefined ? newLoadedValue : initial);
|
|
40
|
+
|
|
41
|
+
load();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
useEffect(
|
|
45
|
+
() => {
|
|
46
|
+
if (previousCacheKey.current !== cacheKey) {
|
|
47
|
+
reset();
|
|
48
|
+
previousCacheKey.current = cacheKey;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[reset, cacheKey],
|
|
52
|
+
);
|
|
33
53
|
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
useEffect(
|
|
55
|
+
() => {
|
|
56
|
+
if (initialRef.current !== undefined) {
|
|
57
|
+
load();
|
|
36
58
|
}
|
|
37
59
|
|
|
38
60
|
return () => {
|
|
39
61
|
mountedRef.current = false;
|
|
40
62
|
};
|
|
41
63
|
},
|
|
42
|
-
[],
|
|
64
|
+
[load],
|
|
43
65
|
);
|
|
44
66
|
|
|
45
67
|
if (value === undefined) {
|
|
@@ -48,3 +70,15 @@ export function useLoader<R>({initial, ...options}: CacheOptions<R>): R {
|
|
|
48
70
|
|
|
49
71
|
return value;
|
|
50
72
|
}
|
|
73
|
+
|
|
74
|
+
type Callback = () => void;
|
|
75
|
+
|
|
76
|
+
function useStableCallback(callback: Callback): Callback {
|
|
77
|
+
const ref = useRef<Callback>();
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
ref.current = callback;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return useCallback(() => { ref.current?.(); }, []);
|
|
84
|
+
}
|