@croct/plug-react 0.8.1 → 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 +2 -10
- 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
package/hooks/useContent.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type UseContentOptions<I, F> = FetchOptions & {
|
|
|
6
6
|
initial?: I;
|
|
7
7
|
cacheKey?: string;
|
|
8
8
|
expiration?: number;
|
|
9
|
+
staleWhileLoading?: boolean;
|
|
9
10
|
};
|
|
10
11
|
type UseContentHook = {
|
|
11
12
|
<P extends JsonObject, I = P, F = P>(id: keyof VersionedSlotMap extends never ? string : never, options?: UseContentOptions<I, F>): P | I | F;
|
package/hooks/useContent.js
CHANGED
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.useContent = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
4
5
|
const useLoader_1 = require("./useLoader");
|
|
5
6
|
const useCroct_1 = require("./useCroct");
|
|
6
7
|
const ssr_polyfills_1 = require("../ssr-polyfills");
|
|
7
8
|
const hash_1 = require("../hash");
|
|
8
9
|
function useCsrContent(id, options = {}) {
|
|
9
10
|
var _a;
|
|
10
|
-
const { fallback,
|
|
11
|
+
const { fallback, cacheKey, expiration, initial: initialContent, staleWhileLoading = false, ...fetchOptions } = options;
|
|
12
|
+
const [initial, setInitial] = (0, react_1.useState)(initialContent);
|
|
11
13
|
const { preferredLocale } = fetchOptions;
|
|
12
14
|
const croct = (0, useCroct_1.useCroct)();
|
|
13
|
-
|
|
15
|
+
const result = (0, useLoader_1.useLoader)({
|
|
14
16
|
cacheKey: (0, hash_1.hash)(`useContent:${cacheKey !== null && cacheKey !== void 0 ? cacheKey : ''}`
|
|
15
17
|
+ `:${id}`
|
|
16
18
|
+ `:${preferredLocale !== null && preferredLocale !== void 0 ? preferredLocale : ''}`
|
|
17
|
-
+ `:${JSON.stringify((_a = fetchOptions.attributes) !== null && _a !== void 0 ? _a :
|
|
19
|
+
+ `:${JSON.stringify((_a = fetchOptions.attributes) !== null && _a !== void 0 ? _a : {})}`),
|
|
18
20
|
loader: () => croct.fetch(id, fetchOptions).then(({ content }) => content),
|
|
19
21
|
initial: initial,
|
|
20
22
|
fallback: fallback,
|
|
21
23
|
expiration: expiration,
|
|
22
24
|
});
|
|
25
|
+
(0, react_1.useEffect)(() => {
|
|
26
|
+
if (staleWhileLoading) {
|
|
27
|
+
setInitial(current => {
|
|
28
|
+
if (current !== result) {
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
return current;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}, [result, staleWhileLoading]);
|
|
35
|
+
return result;
|
|
23
36
|
}
|
|
24
37
|
function useSsrContent(_, { initial } = {}) {
|
|
25
38
|
if (initial === undefined) {
|
package/hooks/useContent.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useContent.js","sourceRoot":"","sources":["../src/hooks/useContent.ts"],"names":[],"mappings":";;;AAGA,2CAAsC;AACtC,yCAAoC;AACpC,oDAAuC;AACvC,kCAA6B;
|
|
1
|
+
{"version":3,"file":"useContent.js","sourceRoot":"","sources":["../src/hooks/useContent.ts"],"names":[],"mappings":";;;AAGA,iCAA0C;AAC1C,2CAAsC;AACtC,yCAAoC;AACpC,oDAAuC;AACvC,kCAA6B;AAU7B,SAAS,aAAa,CAClB,EAAmB,EACnB,UAAmC,EAAE;;IAErC,MAAM,EACF,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,OAAO,EAAE,cAAc,EACvB,iBAAiB,GAAG,KAAK,EACzB,GAAG,YAAY,EAClB,GAAG,OAAO,CAAC;IAEZ,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,IAAA,gBAAQ,EAAkC,cAAc,CAAC,CAAC;IACxF,MAAM,EAAC,eAAe,EAAC,GAAG,YAAY,CAAC;IACvC,MAAM,KAAK,GAAG,IAAA,mBAAQ,GAAE,CAAC;IAEzB,MAAM,MAAM,GAAwB,IAAA,qBAAS,EAAC;QAC1C,QAAQ,EAAE,IAAA,WAAI,EACV,cAAc,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,EAAE,EAAE;cAC5B,IAAI,EAAE,EAAE;cACR,IAAI,eAAe,aAAf,eAAe,cAAf,eAAe,GAAI,EAAE,EAAE;cAC3B,IAAI,IAAI,CAAC,SAAS,CAAC,MAAA,YAAY,CAAC,UAAU,mCAAI,EAAE,CAAC,EAAE,CACxD;QACD,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,EAAC,OAAO,EAAC,EAAE,EAAE,CAAC,OAAO,CAAC;QACxE,OAAO,EAAE,OAAO;QAChB,QAAQ,EAAE,QAAQ;QAClB,UAAU,EAAE,UAAU;KACzB,CAAC,CAAC;IAEH,IAAA,iBAAS,EACL,GAAG,EAAE;QACD,IAAI,iBAAiB,EAAE,CAAC;YACpB,UAAU,CAAC,OAAO,CAAC,EAAE;gBACjB,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;oBACrB,OAAO,MAAM,CAAC;gBAClB,CAAC;gBAED,OAAO,OAAO,CAAC;YACnB,CAAC,CAAC,CAAC;QACP,CAAC;IACL,CAAC,EACD,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAC9B,CAAC;IAEF,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,SAAS,aAAa,CAClB,CAAkB,EAClB,EAAC,OAAO,KAA6B,EAAE;IAEvC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACX,mEAAmE;cACjE,iEAAiE,CACtE,CAAC;IACN,CAAC;IAED,OAAO,OAAO,CAAC;AACnB,CAAC;AA6BY,QAAA,UAAU,GAAmB,IAAA,qBAAK,GAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC"}
|
package/hooks/useEvaluation.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type UseEvaluationOptions<I, F> = EvaluationOptions & {
|
|
|
5
5
|
fallback?: F;
|
|
6
6
|
cacheKey?: string;
|
|
7
7
|
expiration?: number;
|
|
8
|
+
staleWhileLoading?: boolean;
|
|
8
9
|
};
|
|
9
10
|
type UseEvaluationHook = <T extends JsonValue, I = T, F = T>(query: string, options?: UseEvaluationOptions<I, F>) => T | I | F;
|
|
10
11
|
export declare const useEvaluation: UseEvaluationHook;
|
package/hooks/useEvaluation.js
CHANGED
|
@@ -1,32 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.useEvaluation = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
4
5
|
const useLoader_1 = require("./useLoader");
|
|
5
6
|
const useCroct_1 = require("./useCroct");
|
|
6
7
|
const ssr_polyfills_1 = require("../ssr-polyfills");
|
|
7
8
|
const hash_1 = require("../hash");
|
|
8
|
-
function cleanEvaluationOptions(options) {
|
|
9
|
-
const result = {};
|
|
10
|
-
for (const [key, value] of Object.entries(options)) {
|
|
11
|
-
if (value !== undefined) {
|
|
12
|
-
result[key] = value;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return result;
|
|
16
|
-
}
|
|
17
9
|
function useCsrEvaluation(query, options = {}) {
|
|
18
10
|
var _a;
|
|
19
|
-
const { cacheKey, fallback,
|
|
11
|
+
const { cacheKey, fallback, expiration, staleWhileLoading = false, initial: initialValue, ...evaluationOptions } = options;
|
|
12
|
+
const [initial, setInitial] = (0, react_1.useState)(initialValue);
|
|
20
13
|
const croct = (0, useCroct_1.useCroct)();
|
|
21
|
-
|
|
14
|
+
const result = (0, useLoader_1.useLoader)({
|
|
22
15
|
cacheKey: (0, hash_1.hash)(`useEvaluation:${cacheKey !== null && cacheKey !== void 0 ? cacheKey : ''}`
|
|
23
16
|
+ `:${query}`
|
|
24
|
-
+ `:${JSON.stringify((_a = options.attributes) !== null && _a !== void 0 ? _a :
|
|
17
|
+
+ `:${JSON.stringify((_a = options.attributes) !== null && _a !== void 0 ? _a : {})}`),
|
|
25
18
|
loader: () => croct.evaluate(query, cleanEvaluationOptions(evaluationOptions)),
|
|
26
19
|
initial: initial,
|
|
27
20
|
fallback: fallback,
|
|
28
21
|
expiration: expiration,
|
|
29
22
|
});
|
|
23
|
+
(0, react_1.useEffect)(() => {
|
|
24
|
+
if (staleWhileLoading) {
|
|
25
|
+
setInitial(current => {
|
|
26
|
+
if (current !== result) {
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
return current;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}, [result, staleWhileLoading]);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
function cleanEvaluationOptions(options) {
|
|
36
|
+
const result = {};
|
|
37
|
+
for (const [key, value] of Object.entries(options)) {
|
|
38
|
+
if (value !== undefined) {
|
|
39
|
+
result[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
30
43
|
}
|
|
31
44
|
function useSsrEvaluation(_, { initial } = {}) {
|
|
32
45
|
if (initial === undefined) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useEvaluation.js","sourceRoot":"","sources":["../src/hooks/useEvaluation.ts"],"names":[],"mappings":";;;AAEA,2CAAsC;AACtC,yCAAoC;AACpC,oDAAuC;AACvC,kCAA6B;
|
|
1
|
+
{"version":3,"file":"useEvaluation.js","sourceRoot":"","sources":["../src/hooks/useEvaluation.ts"],"names":[],"mappings":";;;AAEA,iCAA0C;AAC1C,2CAAsC;AACtC,yCAAoC;AACpC,oDAAuC;AACvC,kCAA6B;AAe7B,SAAS,gBAAgB,CACrB,KAAa,EACb,UAAsC,EAAE;;IAExC,MAAM,EACF,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,iBAAiB,GAAG,KAAK,EACzB,OAAO,EAAE,YAAY,EACrB,GAAG,iBAAiB,EACvB,GAAG,OAAO,CAAC;IAEZ,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,IAAA,gBAAQ,EAAwB,YAAY,CAAC,CAAC;IAC5E,MAAM,KAAK,GAAG,IAAA,mBAAQ,GAAE,CAAC;IAEzB,MAAM,MAAM,GAAG,IAAA,qBAAS,EAAY;QAChC,QAAQ,EAAE,IAAA,WAAI,EACV,iBAAiB,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,EAAE,EAAE;cAC/B,IAAI,KAAK,EAAE;cACX,IAAI,IAAI,CAAC,SAAS,CAAC,MAAA,OAAO,CAAC,UAAU,mCAAI,EAAE,CAAC,EAAE,CACnD;QACD,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAgB,KAAK,EAAE,sBAAsB,CAAC,iBAAiB,CAAC,CAAC;QAC7F,OAAO,EAAE,OAAO;QAChB,QAAQ,EAAE,QAAQ;QAClB,UAAU,EAAE,UAAU;KACzB,CAAC,CAAC;IAEH,IAAA,iBAAS,EACL,GAAG,EAAE;QACD,IAAI,iBAAiB,EAAE,CAAC;YACpB,UAAU,CAAC,OAAO,CAAC,EAAE;gBACjB,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;oBACrB,OAAO,MAAM,CAAC;gBAClB,CAAC;gBAED,OAAO,OAAO,CAAC;YACnB,CAAC,CAAC,CAAC;QACP,CAAC;IACL,CAAC,EACD,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAC9B,CAAC;IAEF,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,SAAS,sBAAsB,CAAC,OAA0B;IACtD,MAAM,MAAM,GAAsB,EAAE,CAAC;IAErC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAA0C,EAAE,CAAC;QAC1F,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACtB,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACxB,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,SAAS,gBAAgB,CACrB,CAAS,EACT,EAAC,OAAO,KAAgC,EAAE;IAE1C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACX,iEAAiE;cAC/D,sEAAsE,CAC3E,CAAC;IACN,CAAC;IAED,OAAO,OAAO,CAAC;AACnB,CAAC;AAEY,QAAA,aAAa,GAAsB,IAAA,qBAAK,GAAE,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,gBAAgB,CAAC"}
|
package/hooks/useLoader.js
CHANGED
|
@@ -6,35 +6,58 @@ const Cache_1 = require("./Cache");
|
|
|
6
6
|
const cache = new Cache_1.Cache(60 * 1000);
|
|
7
7
|
function useLoader({ initial, ...options }) {
|
|
8
8
|
var _a;
|
|
9
|
-
const
|
|
9
|
+
const { cacheKey } = options;
|
|
10
|
+
const loadedValue = (_a = cache.get(cacheKey)) === null || _a === void 0 ? void 0 : _a.result;
|
|
10
11
|
const [value, setValue] = (0, react_1.useState)(loadedValue !== undefined ? loadedValue : initial);
|
|
11
12
|
const mountedRef = (0, react_1.useRef)(true);
|
|
12
|
-
const
|
|
13
|
-
(0, react_1.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
setValue(undefined);
|
|
13
|
+
const initialRef = (0, react_1.useRef)(initial);
|
|
14
|
+
const previousCacheKey = (0, react_1.useRef)(cacheKey);
|
|
15
|
+
const load = useStableCallback(() => {
|
|
16
|
+
try {
|
|
17
|
+
setValue(cache.load(options));
|
|
18
|
+
}
|
|
19
|
+
catch (result) {
|
|
20
|
+
if (result instanceof Promise) {
|
|
21
|
+
result.then((resolvedValue) => {
|
|
22
|
+
if (mountedRef.current) {
|
|
23
|
+
setValue(resolvedValue);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
28
26
|
return;
|
|
29
27
|
}
|
|
28
|
+
setValue(undefined);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const reset = useStableCallback(() => {
|
|
32
|
+
var _a;
|
|
33
|
+
const newLoadedValue = (_a = cache.get(cacheKey)) === null || _a === void 0 ? void 0 : _a.result;
|
|
34
|
+
setValue(newLoadedValue !== undefined ? newLoadedValue : initial);
|
|
35
|
+
load();
|
|
36
|
+
});
|
|
37
|
+
(0, react_1.useEffect)(() => {
|
|
38
|
+
if (previousCacheKey.current !== cacheKey) {
|
|
39
|
+
reset();
|
|
40
|
+
previousCacheKey.current = cacheKey;
|
|
41
|
+
}
|
|
42
|
+
}, [reset, cacheKey]);
|
|
43
|
+
(0, react_1.useEffect)(() => {
|
|
44
|
+
if (initialRef.current !== undefined) {
|
|
45
|
+
load();
|
|
30
46
|
}
|
|
31
47
|
return () => {
|
|
32
48
|
mountedRef.current = false;
|
|
33
49
|
};
|
|
34
|
-
}, []);
|
|
50
|
+
}, [load]);
|
|
35
51
|
if (value === undefined) {
|
|
36
52
|
return cache.load(options);
|
|
37
53
|
}
|
|
38
54
|
return value;
|
|
39
55
|
}
|
|
56
|
+
function useStableCallback(callback) {
|
|
57
|
+
const ref = (0, react_1.useRef)();
|
|
58
|
+
(0, react_1.useEffect)(() => {
|
|
59
|
+
ref.current = callback;
|
|
60
|
+
});
|
|
61
|
+
return (0, react_1.useCallback)(() => { var _a; (_a = ref.current) === null || _a === void 0 ? void 0 : _a.call(ref); }, []);
|
|
62
|
+
}
|
|
40
63
|
//# sourceMappingURL=useLoader.js.map
|
package/hooks/useLoader.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLoader.js","sourceRoot":"","sources":["../src/hooks/useLoader.ts"],"names":[],"mappings":";;AASA,
|
|
1
|
+
{"version":3,"file":"useLoader.js","sourceRoot":"","sources":["../src/hooks/useLoader.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@croct/plug-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "React components and hooks to plug your React applications into Croct.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Croct",
|
|
@@ -28,9 +28,7 @@
|
|
|
28
28
|
"lint": "eslint 'src/**/*.ts' 'src/**/*.tsx'",
|
|
29
29
|
"test": "jest -c jest.config.js --coverage",
|
|
30
30
|
"validate": "tsc --noEmit",
|
|
31
|
-
"build": "tsc -p tsconfig.build.json"
|
|
32
|
-
"storybook": "start-storybook -s ./.storybook/static -p 6006 --no-manager-cache",
|
|
33
|
-
"build-storybook": "build-storybook -s ./.storybook/static"
|
|
31
|
+
"build": "tsc -p tsconfig.build.json"
|
|
34
32
|
},
|
|
35
33
|
"peerDependencies": {
|
|
36
34
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
|
@@ -46,12 +44,6 @@
|
|
|
46
44
|
"@babel/preset-react": "^7.24.7",
|
|
47
45
|
"@babel/preset-typescript": "^7.24.7",
|
|
48
46
|
"@croct/eslint-plugin": "^0.7.1",
|
|
49
|
-
"@storybook/addon-actions": "^8.2.9",
|
|
50
|
-
"@storybook/addon-essentials": "^8.2.9",
|
|
51
|
-
"@storybook/addon-links": "^8.2.9",
|
|
52
|
-
"@storybook/builder-webpack5": "^8.2.9",
|
|
53
|
-
"@storybook/manager-webpack5": "^6.5.16",
|
|
54
|
-
"@storybook/react": "^8.2.9",
|
|
55
47
|
"@testing-library/jest-dom": "^6.5.0",
|
|
56
48
|
"@testing-library/react": "^16.0.1",
|
|
57
49
|
"@types/jest": "^29.5.12",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {renderHook} from '@testing-library/react';
|
|
1
|
+
import {renderHook, waitFor} from '@testing-library/react';
|
|
2
2
|
import {Plug} from '@croct/plug';
|
|
3
3
|
import {useCroct} from './useCroct';
|
|
4
4
|
import {useLoader} from './useLoader';
|
|
@@ -20,15 +20,19 @@ jest.mock(
|
|
|
20
20
|
);
|
|
21
21
|
|
|
22
22
|
describe('useContent (CSR)', () => {
|
|
23
|
-
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
jest.resetAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should fetch the content', () => {
|
|
24
28
|
const fetch: Plug['fetch'] = jest.fn().mockResolvedValue({
|
|
25
|
-
|
|
26
|
-
title: 'loaded',
|
|
27
|
-
},
|
|
29
|
+
content: {},
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
jest.mocked(useCroct).mockReturnValue({fetch: fetch} as Plug);
|
|
31
|
-
jest.mocked(useLoader).mockReturnValue(
|
|
33
|
+
jest.mocked(useLoader).mockReturnValue({
|
|
34
|
+
title: 'foo',
|
|
35
|
+
});
|
|
32
36
|
|
|
33
37
|
const slotId = 'home-banner@1';
|
|
34
38
|
const preferredLocale = 'en';
|
|
@@ -67,6 +71,113 @@ describe('useContent (CSR)', () => {
|
|
|
67
71
|
attributes: attributes,
|
|
68
72
|
});
|
|
69
73
|
|
|
70
|
-
expect(result.current).
|
|
74
|
+
expect(result.current).toEqual({title: 'foo'});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should use the initial value when the cache key changes if the stale-while-loading flag is false', async () => {
|
|
78
|
+
const key = {
|
|
79
|
+
current: 'initial',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const fetch: Plug['fetch'] = jest.fn().mockResolvedValue({content: {}});
|
|
83
|
+
|
|
84
|
+
jest.mocked(useCroct).mockReturnValue({fetch: fetch} as Plug);
|
|
85
|
+
|
|
86
|
+
jest.mocked(useLoader).mockImplementation(
|
|
87
|
+
() => ({title: key.current === 'initial' ? 'first' : 'second'}),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const slotId = 'home-banner@1';
|
|
91
|
+
|
|
92
|
+
const {result, rerender} = renderHook(
|
|
93
|
+
() => useContent<{title: string}>(slotId, {
|
|
94
|
+
cacheKey: key.current,
|
|
95
|
+
initial: {
|
|
96
|
+
title: 'initial',
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(useCroct).toHaveBeenCalled();
|
|
102
|
+
|
|
103
|
+
expect(useLoader).toHaveBeenCalledWith(expect.objectContaining({
|
|
104
|
+
cacheKey: hash(`useContent:${key.current}:${slotId}::${JSON.stringify({})}`),
|
|
105
|
+
initial: {
|
|
106
|
+
title: 'initial',
|
|
107
|
+
},
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
await waitFor(() => expect(result.current).toEqual({title: 'first'}));
|
|
111
|
+
|
|
112
|
+
key.current = 'next';
|
|
113
|
+
|
|
114
|
+
rerender();
|
|
115
|
+
|
|
116
|
+
expect(useLoader).toHaveBeenCalledWith(expect.objectContaining({
|
|
117
|
+
cacheKey: hash(`useContent:${key.current}:${slotId}::${JSON.stringify({})}`),
|
|
118
|
+
initial: {
|
|
119
|
+
title: 'initial',
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
await waitFor(() => expect(result.current).toEqual({title: 'second'}));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should use the last fetched content as initial value if the stale-while-loading flag is true', async () => {
|
|
127
|
+
const key = {
|
|
128
|
+
current: 'initial',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const fetch: Plug['fetch'] = jest.fn().mockResolvedValue({content: {}});
|
|
132
|
+
|
|
133
|
+
jest.mocked(useCroct).mockReturnValue({fetch: fetch} as Plug);
|
|
134
|
+
|
|
135
|
+
const firstResult = {
|
|
136
|
+
title: 'first',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const secondResult = {
|
|
140
|
+
title: 'second',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
jest.mocked(useLoader).mockImplementation(
|
|
144
|
+
() => (key.current === 'initial' ? firstResult : secondResult),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const slotId = 'home-banner@1';
|
|
148
|
+
|
|
149
|
+
const {result, rerender} = renderHook(
|
|
150
|
+
() => useContent<{title: string}>(slotId, {
|
|
151
|
+
cacheKey: key.current,
|
|
152
|
+
initial: {
|
|
153
|
+
title: 'initial',
|
|
154
|
+
},
|
|
155
|
+
staleWhileLoading: true,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
expect(useCroct).toHaveBeenCalled();
|
|
160
|
+
|
|
161
|
+
expect(useLoader).toHaveBeenCalledWith(expect.objectContaining({
|
|
162
|
+
cacheKey: hash(`useContent:${key.current}:${slotId}::${JSON.stringify({})}`),
|
|
163
|
+
initial: {
|
|
164
|
+
title: 'initial',
|
|
165
|
+
},
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
await waitFor(() => expect(result.current).toEqual({title: 'first'}));
|
|
169
|
+
|
|
170
|
+
key.current = 'next';
|
|
171
|
+
|
|
172
|
+
rerender();
|
|
173
|
+
|
|
174
|
+
expect(useLoader).toHaveBeenCalledWith(expect.objectContaining({
|
|
175
|
+
cacheKey: hash(`useContent:${key.current}:${slotId}::${JSON.stringify({})}`),
|
|
176
|
+
initial: {
|
|
177
|
+
title: 'first',
|
|
178
|
+
},
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
await waitFor(() => expect(result.current).toEqual({title: 'second'}));
|
|
71
182
|
});
|
|
72
183
|
});
|
package/src/hooks/useContent.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {SlotContent, VersionedSlotId, VersionedSlotMap} from '@croct/plug/slot';
|
|
2
2
|
import {JsonObject} from '@croct/plug/sdk/json';
|
|
3
3
|
import {FetchOptions} from '@croct/plug/plug';
|
|
4
|
+
import {useEffect, useState} from 'react';
|
|
4
5
|
import {useLoader} from './useLoader';
|
|
5
6
|
import {useCroct} from './useCroct';
|
|
6
7
|
import {isSsr} from '../ssr-polyfills';
|
|
@@ -11,28 +12,55 @@ export type UseContentOptions<I, F> = FetchOptions & {
|
|
|
11
12
|
initial?: I,
|
|
12
13
|
cacheKey?: string,
|
|
13
14
|
expiration?: number,
|
|
15
|
+
staleWhileLoading?: boolean,
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
function useCsrContent<I, F>(
|
|
17
19
|
id: VersionedSlotId,
|
|
18
20
|
options: UseContentOptions<I, F> = {},
|
|
19
21
|
): SlotContent | I | F {
|
|
20
|
-
const {
|
|
22
|
+
const {
|
|
23
|
+
fallback,
|
|
24
|
+
cacheKey,
|
|
25
|
+
expiration,
|
|
26
|
+
initial: initialContent,
|
|
27
|
+
staleWhileLoading = false,
|
|
28
|
+
...fetchOptions
|
|
29
|
+
} = options;
|
|
30
|
+
|
|
31
|
+
const [initial, setInitial] = useState<SlotContent | I | F | undefined>(initialContent);
|
|
21
32
|
const {preferredLocale} = fetchOptions;
|
|
22
33
|
const croct = useCroct();
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
const result: SlotContent | I | F = useLoader({
|
|
25
36
|
cacheKey: hash(
|
|
26
37
|
`useContent:${cacheKey ?? ''}`
|
|
27
38
|
+ `:${id}`
|
|
28
39
|
+ `:${preferredLocale ?? ''}`
|
|
29
|
-
+ `:${JSON.stringify(fetchOptions.attributes ??
|
|
40
|
+
+ `:${JSON.stringify(fetchOptions.attributes ?? {})}`,
|
|
30
41
|
),
|
|
31
42
|
loader: () => croct.fetch(id, fetchOptions).then(({content}) => content),
|
|
32
43
|
initial: initial,
|
|
33
44
|
fallback: fallback,
|
|
34
45
|
expiration: expiration,
|
|
35
46
|
});
|
|
47
|
+
|
|
48
|
+
useEffect(
|
|
49
|
+
() => {
|
|
50
|
+
if (staleWhileLoading) {
|
|
51
|
+
setInitial(current => {
|
|
52
|
+
if (current !== result) {
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return current;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
[result, staleWhileLoading],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return result;
|
|
36
64
|
}
|
|
37
65
|
|
|
38
66
|
function useSsrContent<I, F>(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {renderHook} from '@testing-library/react';
|
|
1
|
+
import {renderHook, waitFor} from '@testing-library/react';
|
|
2
2
|
import {EvaluationOptions} from '@croct/sdk/facade/evaluatorFacade';
|
|
3
3
|
import {Plug} from '@croct/plug';
|
|
4
4
|
import {useEvaluation} from './useEvaluation';
|
|
@@ -22,7 +22,6 @@ jest.mock(
|
|
|
22
22
|
|
|
23
23
|
describe('useEvaluation', () => {
|
|
24
24
|
beforeEach(() => {
|
|
25
|
-
jest.resetModules();
|
|
26
25
|
jest.resetAllMocks();
|
|
27
26
|
});
|
|
28
27
|
|
|
@@ -91,4 +90,91 @@ describe('useEvaluation', () => {
|
|
|
91
90
|
|
|
92
91
|
expect(evaluate).toHaveBeenCalledWith(query, {});
|
|
93
92
|
});
|
|
93
|
+
|
|
94
|
+
it('should use the initial value when the cache key changes if the stale-while-loading flag is false', async () => {
|
|
95
|
+
const key = {
|
|
96
|
+
current: 'initial',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const evaluate: Plug['evaluate'] = jest.fn();
|
|
100
|
+
|
|
101
|
+
jest.mocked(useCroct).mockReturnValue({evaluate: evaluate} as Plug);
|
|
102
|
+
|
|
103
|
+
jest.mocked(useLoader).mockImplementation(
|
|
104
|
+
() => (key.current === 'initial' ? 'first' : 'second'),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const query = 'location';
|
|
108
|
+
|
|
109
|
+
const {result, rerender} = renderHook(
|
|
110
|
+
() => useEvaluation(query, {
|
|
111
|
+
cacheKey: key.current,
|
|
112
|
+
initial: 'initial',
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(useCroct).toHaveBeenCalled();
|
|
117
|
+
|
|
118
|
+
expect(useLoader).toHaveBeenCalledWith(expect.objectContaining({
|
|
119
|
+
cacheKey: hash(`useEvaluation:${key.current}:${query}:${JSON.stringify({})}`),
|
|
120
|
+
initial: 'initial',
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
await waitFor(() => expect(result.current).toEqual('first'));
|
|
124
|
+
|
|
125
|
+
key.current = 'next';
|
|
126
|
+
|
|
127
|
+
rerender();
|
|
128
|
+
|
|
129
|
+
expect(useLoader).toHaveBeenCalledWith(expect.objectContaining({
|
|
130
|
+
cacheKey: hash(`useEvaluation:${key.current}:${query}:${JSON.stringify({})}`),
|
|
131
|
+
initial: 'initial',
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
await waitFor(() => expect(result.current).toEqual('second'));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should use the last evaluation result if the stale-while-loading flag is true', async () => {
|
|
138
|
+
const key = {
|
|
139
|
+
current: 'initial',
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const evaluate: Plug['evaluate'] = jest.fn();
|
|
143
|
+
|
|
144
|
+
jest.mocked(useCroct).mockReturnValue({evaluate: evaluate} as Plug);
|
|
145
|
+
|
|
146
|
+
jest.mocked(useLoader).mockImplementation(
|
|
147
|
+
() => (key.current === 'initial' ? 'first' : 'second'),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const query = 'location';
|
|
151
|
+
|
|
152
|
+
const {result, rerender} = renderHook(
|
|
153
|
+
() => useEvaluation(query, {
|
|
154
|
+
cacheKey: key.current,
|
|
155
|
+
initial: 'initial',
|
|
156
|
+
staleWhileLoading: true,
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(useCroct).toHaveBeenCalled();
|
|
161
|
+
|
|
162
|
+
expect(useLoader).toHaveBeenCalledWith(expect.objectContaining({
|
|
163
|
+
cacheKey: hash(`useEvaluation:${key.current}:${query}:${JSON.stringify({})}`),
|
|
164
|
+
initial: 'initial',
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
await waitFor(() => expect(result.current).toEqual('first'));
|
|
168
|
+
|
|
169
|
+
key.current = 'next';
|
|
170
|
+
|
|
171
|
+
rerender();
|
|
172
|
+
|
|
173
|
+
expect(useLoader).toHaveBeenCalledWith(expect.objectContaining({
|
|
174
|
+
cacheKey: hash(`useEvaluation:${key.current}:${query}:${JSON.stringify({})}`),
|
|
175
|
+
initial: 'first',
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
await waitFor(() => expect(result.current).toEqual('second'));
|
|
179
|
+
});
|
|
94
180
|
});
|
|
@@ -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>(
|