@khanacademy/wonder-blocks-data 6.0.0 → 7.0.1
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/CHANGELOG.md +18 -0
- package/dist/es/index.js +64 -689
- package/dist/index.js +38 -49
- package/legacy-docs.md +1 -1
- package/package.json +3 -3
- package/src/__docs__/exports.use-server-effect.stories.mdx +13 -1
- package/src/hooks/__tests__/use-hydratable-effect.test.js +22 -25
- package/src/hooks/__tests__/use-server-effect.test.js +51 -0
- package/src/hooks/use-hydratable-effect.js +13 -13
- package/src/hooks/use-server-effect.js +30 -5
- package/src/util/abort-error.js +0 -15
package/dist/index.js
CHANGED
|
@@ -82,7 +82,7 @@ module.exports =
|
|
|
82
82
|
/******/
|
|
83
83
|
/******/
|
|
84
84
|
/******/ // Load entry module and return exports
|
|
85
|
-
/******/ return __webpack_require__(__webpack_require__.s =
|
|
85
|
+
/******/ return __webpack_require__(__webpack_require__.s = 27);
|
|
86
86
|
/******/ })
|
|
87
87
|
/************************************************************************/
|
|
88
88
|
/******/ ([
|
|
@@ -885,11 +885,9 @@ class RequestFulfillment {
|
|
|
885
885
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return useHydratableEffect; });
|
|
886
886
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
|
|
887
887
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
|
888
|
-
/* harmony import */ var
|
|
889
|
-
/* harmony import */ var
|
|
890
|
-
/* harmony import */ var
|
|
891
|
-
/* harmony import */ var _use_cached_effect_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(15);
|
|
892
|
-
|
|
888
|
+
/* harmony import */ var _use_server_effect_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14);
|
|
889
|
+
/* harmony import */ var _use_shared_cache_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8);
|
|
890
|
+
/* harmony import */ var _use_cached_effect_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(15);
|
|
893
891
|
|
|
894
892
|
|
|
895
893
|
|
|
@@ -898,7 +896,7 @@ class RequestFulfillment {
|
|
|
898
896
|
/**
|
|
899
897
|
* Policies to define how a hydratable effect should behave client-side.
|
|
900
898
|
*/
|
|
901
|
-
const WhenClientSide = __webpack_require__(
|
|
899
|
+
const WhenClientSide = __webpack_require__(28).Mirrored(["DoNotHydrate", "ExecuteWhenNoResult", "ExecuteWhenNoSuccessResult", "AlwaysExecute"]);
|
|
902
900
|
const DefaultScope = "useHydratableEffect";
|
|
903
901
|
/**
|
|
904
902
|
* Hook to execute an async operation on server and client.
|
|
@@ -922,10 +920,11 @@ const useHydratableEffect = (requestId, handler, options = {}) => {
|
|
|
922
920
|
// When client-side, this will look up any response for hydration; it does
|
|
923
921
|
// not invoke the handler.
|
|
924
922
|
|
|
925
|
-
const serverResult = Object(
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
923
|
+
const serverResult = Object(_use_server_effect_js__WEBPACK_IMPORTED_MODULE_1__[/* useServerEffect */ "a"])(requestId, handler, {
|
|
924
|
+
// Only hydrate if our behavior isn't telling us not to.
|
|
925
|
+
hydrate: clientBehavior !== WhenClientSide.DoNotHydrate,
|
|
926
|
+
skip
|
|
927
|
+
});
|
|
929
928
|
const getDefaultCacheValue = react__WEBPACK_IMPORTED_MODULE_0__["useCallback"](() => {
|
|
930
929
|
// If we don't have a requestId, it's our first render, the one
|
|
931
930
|
// where we hydrated. So defer to our clientBehavior value.
|
|
@@ -952,17 +951,24 @@ const useHydratableEffect = (requestId, handler, options = {}) => {
|
|
|
952
951
|
}
|
|
953
952
|
|
|
954
953
|
return null;
|
|
955
|
-
} // There is no reason for this to change after the first render
|
|
954
|
+
} // There is no reason for this to change after the first render,
|
|
955
|
+
// you might think, but the function closes around serverResult and if
|
|
956
|
+
// the requestId changes, it still returns the hydrate result of the
|
|
957
|
+
// first render of the previous requestId. This then means that the
|
|
958
|
+
// hydrate result is still the same, and the effect is not re-executed
|
|
959
|
+
// because the cache gets incorrectly defaulted.
|
|
960
|
+
// However, we don't want to bother doing anything with this on
|
|
961
|
+
// client behavior changing since that truly is irrelevant.
|
|
956
962
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
957
963
|
|
|
958
|
-
}, []); // Instead of using state, which would be local to just this hook instance,
|
|
964
|
+
}, [serverResult]); // Instead of using state, which would be local to just this hook instance,
|
|
959
965
|
// we use a shared in-memory cache.
|
|
960
966
|
|
|
961
|
-
Object(
|
|
967
|
+
Object(_use_shared_cache_js__WEBPACK_IMPORTED_MODULE_2__[/* useSharedCache */ "b"])(requestId, // The key of the cached item
|
|
962
968
|
scope, // The scope of the cached items
|
|
963
969
|
getDefaultCacheValue); // When we're client-side, we ultimately want the result from this call.
|
|
964
970
|
|
|
965
|
-
const clientResult = Object(
|
|
971
|
+
const clientResult = Object(_use_cached_effect_js__WEBPACK_IMPORTED_MODULE_3__[/* useCachedEffect */ "a"])(requestId, handler, {
|
|
966
972
|
skip,
|
|
967
973
|
onResultChanged,
|
|
968
974
|
retainResultOnChange,
|
|
@@ -1054,7 +1060,7 @@ const InterceptContext = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["create
|
|
|
1054
1060
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
|
|
1055
1061
|
/* harmony import */ var _util_request_tracking_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(7);
|
|
1056
1062
|
/* harmony import */ var _util_ssr_cache_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5);
|
|
1057
|
-
/* harmony import */ var _util_result_from_cache_response_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(
|
|
1063
|
+
/* harmony import */ var _util_result_from_cache_response_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(24);
|
|
1058
1064
|
/* harmony import */ var _use_request_interception_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(16);
|
|
1059
1065
|
|
|
1060
1066
|
|
|
@@ -1078,22 +1084,26 @@ const InterceptContext = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["create
|
|
|
1078
1084
|
*
|
|
1079
1085
|
* The asynchronous action is never invoked on the client-side.
|
|
1080
1086
|
*/
|
|
1081
|
-
const useServerEffect = (requestId, handler,
|
|
1082
|
-
|
|
1087
|
+
const useServerEffect = (requestId, handler, options = {}) => {
|
|
1088
|
+
const {
|
|
1089
|
+
hydrate = true,
|
|
1090
|
+
skip = false
|
|
1091
|
+
} = options; // Plug in to the request interception framework for code that wants
|
|
1083
1092
|
// to use that.
|
|
1093
|
+
|
|
1084
1094
|
const interceptedHandler = Object(_use_request_interception_js__WEBPACK_IMPORTED_MODULE_5__[/* useRequestInterception */ "a"])(requestId, handler); // If we're server-side or hydrating, we'll have a cached entry to use.
|
|
1085
1095
|
// So we get that and use it to initialize our state.
|
|
1086
1096
|
// This works in both hydration and SSR because the very first call to
|
|
1087
1097
|
// this will have cached data in those cases as it will be present on the
|
|
1088
1098
|
// initial render - and subsequent renders on the client it will be null.
|
|
1089
1099
|
|
|
1090
|
-
const cachedResult = _util_ssr_cache_js__WEBPACK_IMPORTED_MODULE_3__[/* SsrCache */ "a"].Default.getEntry(requestId); // We only track data requests when we are server-side
|
|
1091
|
-
// already have a result, as given by the
|
|
1092
|
-
// initial value for the result state).
|
|
1100
|
+
const cachedResult = _util_ssr_cache_js__WEBPACK_IMPORTED_MODULE_3__[/* SsrCache */ "a"].Default.getEntry(requestId); // We only track data requests when we are server-side, we are not skipping
|
|
1101
|
+
// the request, and we don't already have a result, as given by the
|
|
1102
|
+
// cachedData (which is also the initial value for the result state).
|
|
1093
1103
|
|
|
1094
1104
|
const maybeTrack = Object(react__WEBPACK_IMPORTED_MODULE_1__["useContext"])(_util_request_tracking_js__WEBPACK_IMPORTED_MODULE_2__[/* TrackerContext */ "b"]);
|
|
1095
1105
|
|
|
1096
|
-
if (cachedResult == null && _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0__["Server"].isServerSide()) {
|
|
1106
|
+
if (!skip && cachedResult == null && _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_0__["Server"].isServerSide()) {
|
|
1097
1107
|
maybeTrack == null ? void 0 : maybeTrack(requestId, interceptedHandler, hydrate);
|
|
1098
1108
|
} // A null result means there was no result to hydrate.
|
|
1099
1109
|
|
|
@@ -1510,8 +1520,8 @@ const GqlRouter = ({
|
|
|
1510
1520
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
|
|
1511
1521
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
|
1512
1522
|
/* harmony import */ var _util_merge_gql_context_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(18);
|
|
1513
|
-
/* harmony import */ var _use_gql_router_context_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(
|
|
1514
|
-
/* harmony import */ var _util_get_gql_data_from_response_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(
|
|
1523
|
+
/* harmony import */ var _use_gql_router_context_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(25);
|
|
1524
|
+
/* harmony import */ var _util_get_gql_data_from_response_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(26);
|
|
1515
1525
|
|
|
1516
1526
|
|
|
1517
1527
|
|
|
@@ -1555,27 +1565,6 @@ const useGql = (context = {}) => {
|
|
|
1555
1565
|
/* 24 */
|
|
1556
1566
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1557
1567
|
|
|
1558
|
-
"use strict";
|
|
1559
|
-
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return AbortError; });
|
|
1560
|
-
/**
|
|
1561
|
-
* Simple implementation to represent aborting.
|
|
1562
|
-
*
|
|
1563
|
-
* Other frameworks may provide this too, so we won't be sharing this with
|
|
1564
|
-
* the outside world. It's just a utility for test and internal use whenever
|
|
1565
|
-
* we need to represent the concept of aborted things.
|
|
1566
|
-
*/
|
|
1567
|
-
class AbortError extends Error {
|
|
1568
|
-
constructor(message) {
|
|
1569
|
-
super(message);
|
|
1570
|
-
this.name = "AbortError";
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
/***/ }),
|
|
1576
|
-
/* 25 */
|
|
1577
|
-
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1578
|
-
|
|
1579
1568
|
"use strict";
|
|
1580
1569
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return resultFromCachedResponse; });
|
|
1581
1570
|
/* harmony import */ var _status_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4);
|
|
@@ -1613,7 +1602,7 @@ const resultFromCachedResponse = cacheEntry => {
|
|
|
1613
1602
|
};
|
|
1614
1603
|
|
|
1615
1604
|
/***/ }),
|
|
1616
|
-
/*
|
|
1605
|
+
/* 25 */
|
|
1617
1606
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1618
1607
|
|
|
1619
1608
|
"use strict";
|
|
@@ -1665,7 +1654,7 @@ const useGqlRouterContext = (contextOverrides = {}) => {
|
|
|
1665
1654
|
};
|
|
1666
1655
|
|
|
1667
1656
|
/***/ }),
|
|
1668
|
-
/*
|
|
1657
|
+
/* 26 */
|
|
1669
1658
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1670
1659
|
|
|
1671
1660
|
"use strict";
|
|
@@ -1735,7 +1724,7 @@ const getGqlDataFromResponse = async response => {
|
|
|
1735
1724
|
};
|
|
1736
1725
|
|
|
1737
1726
|
/***/ }),
|
|
1738
|
-
/*
|
|
1727
|
+
/* 27 */
|
|
1739
1728
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1740
1729
|
|
|
1741
1730
|
"use strict";
|
|
@@ -1882,7 +1871,7 @@ const removeAllFromCache = predicate => _util_ssr_cache_js__WEBPACK_IMPORTED_MOD
|
|
|
1882
1871
|
|
|
1883
1872
|
|
|
1884
1873
|
/***/ }),
|
|
1885
|
-
/*
|
|
1874
|
+
/* 28 */
|
|
1886
1875
|
/***/ (function(module, exports, __webpack_require__) {
|
|
1887
1876
|
|
|
1888
1877
|
"use strict";
|
package/legacy-docs.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
Documentation for Wonder Blocks Data is now in Storybook.
|
|
2
2
|
|
|
3
|
-
Either run `yarn start:storybook` locally, or visit the the stories for the `
|
|
3
|
+
Either run `yarn start:storybook` locally, or visit the the stories for the `main` branch on [Chromatic](https://main--5e1bf4b385e3fb0020b7073c.chromatic.com).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-data",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.1",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@babel/runtime": "^7.16.3",
|
|
17
|
-
"@khanacademy/wonder-blocks-core": "^4.3.
|
|
17
|
+
"@khanacademy/wonder-blocks-core": "^4.3.1"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"@khanacademy/wonder-stuff-core": "^0.1.2",
|
|
21
21
|
"react": "16.14.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"wb-dev-build-settings": "^0.
|
|
24
|
+
"wb-dev-build-settings": "^0.4.0"
|
|
25
25
|
},
|
|
26
26
|
"author": "",
|
|
27
27
|
"license": "MIT"
|
|
@@ -15,12 +15,24 @@ import {Meta} from "@storybook/addon-docs";
|
|
|
15
15
|
function useServerEffect<TData: ValidCacheData>(
|
|
16
16
|
requestId: string,
|
|
17
17
|
handler: () => Promise<TData>,
|
|
18
|
-
|
|
18
|
+
options?: ServerEffectOptions,
|
|
19
19
|
): ?Result<TData>;
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
The `useServerEffect` hook is an integral part of server-side rendering. It has different behavior depending on whether it is running on the server (and in what context) or the client.
|
|
23
23
|
|
|
24
|
+
```ts
|
|
25
|
+
type ServerEffectOptions = {|
|
|
26
|
+
skip?: boolean,
|
|
27
|
+
hydrate?: boolean,
|
|
28
|
+
|};
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Option | Default | Description |
|
|
32
|
+
| ------ | ------- | ----------- |
|
|
33
|
+
| `hydrate` | `true` | When `true`, the result of the effect when fulfilled using Wonder Blocks Data will be stored in the hydration cache for hydrating client-side; otherwise, the result will be stored in the server-side-only cache. |
|
|
34
|
+
| `skip` | `false` | When `true`, the effect will not be tracked for fulfillment; otherwise, the effect will be tracked for fulfillment. |
|
|
35
|
+
|
|
24
36
|
## Server-side behavior
|
|
25
37
|
|
|
26
38
|
First, this hook checks the server-side rendering cache for the request identifier; if it finds a cached value, it will return that.
|
|
@@ -107,34 +107,11 @@ describe("#useHydratableEffect", () => {
|
|
|
107
107
|
expect(useServerEffectSpy).toHaveBeenCalledWith(
|
|
108
108
|
"ID",
|
|
109
109
|
fakeHandler,
|
|
110
|
-
hydrate,
|
|
110
|
+
{hydrate, skip: false},
|
|
111
111
|
);
|
|
112
112
|
},
|
|
113
113
|
);
|
|
114
114
|
|
|
115
|
-
it("should pass an abort handler to useServerEffect when skip is true", async () => {
|
|
116
|
-
// Arrange
|
|
117
|
-
jest.spyOn(
|
|
118
|
-
UseRequestInterception,
|
|
119
|
-
"useRequestInterception",
|
|
120
|
-
).mockReturnValue(jest.fn());
|
|
121
|
-
const fakeHandler = jest.fn();
|
|
122
|
-
const useServerEffectSpy = jest
|
|
123
|
-
.spyOn(UseServerEffect, "useServerEffect")
|
|
124
|
-
.mockReturnValue(null);
|
|
125
|
-
|
|
126
|
-
// Act
|
|
127
|
-
serverRenderHook(() =>
|
|
128
|
-
useHydratableEffect("ID", fakeHandler, {skip: true}),
|
|
129
|
-
);
|
|
130
|
-
const underTest = useServerEffectSpy.mock.calls[0][1]();
|
|
131
|
-
|
|
132
|
-
// Assert
|
|
133
|
-
await expect(underTest).rejects.toMatchInlineSnapshot(
|
|
134
|
-
`[AbortError: skipped]`,
|
|
135
|
-
);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
115
|
it.each`
|
|
139
116
|
scope | expectedScope
|
|
140
117
|
${undefined} | ${"useHydratableEffect"}
|
|
@@ -247,7 +224,7 @@ describe("#useHydratableEffect", () => {
|
|
|
247
224
|
expect(useServerEffectSpy).toHaveBeenCalledWith(
|
|
248
225
|
"ID",
|
|
249
226
|
fakeHandler,
|
|
250
|
-
hydrate,
|
|
227
|
+
{hydrate, skip: false},
|
|
251
228
|
);
|
|
252
229
|
},
|
|
253
230
|
);
|
|
@@ -413,6 +390,26 @@ describe("#useHydratableEffect", () => {
|
|
|
413
390
|
expect(fakeHandler).toHaveBeenCalledTimes(2);
|
|
414
391
|
});
|
|
415
392
|
|
|
393
|
+
it("should default shared cache to hydrate value for new requestId", async () => {
|
|
394
|
+
// Arrange
|
|
395
|
+
const fakeHandler = jest.fn().mockResolvedValue("data");
|
|
396
|
+
jest.spyOn(UseServerEffect, "useServerEffect")
|
|
397
|
+
.mockReturnValueOnce(Status.success("BADDATA"))
|
|
398
|
+
.mockReturnValue(null);
|
|
399
|
+
|
|
400
|
+
// Act
|
|
401
|
+
const {rerender, result} = clientRenderHook(
|
|
402
|
+
({requestId}) => useHydratableEffect(requestId, fakeHandler),
|
|
403
|
+
{
|
|
404
|
+
initialProps: {requestId: "ID"},
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
rerender({requestId: "ID2"});
|
|
408
|
+
|
|
409
|
+
// Assert
|
|
410
|
+
expect(result.current).toStrictEqual(Status.loading());
|
|
411
|
+
});
|
|
412
|
+
|
|
416
413
|
it("should update shared cache with result when request is fulfilled", async () => {
|
|
417
414
|
// Arrange
|
|
418
415
|
const setCacheFn = jest.fn();
|
|
@@ -109,6 +109,57 @@ describe("#useServerEffect", () => {
|
|
|
109
109
|
);
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
+
it("should not track the intercepted request if skip is true", () => {
|
|
113
|
+
// Arrange
|
|
114
|
+
const fakeHandler = jest.fn();
|
|
115
|
+
const interceptedHandler = jest.fn();
|
|
116
|
+
jest.spyOn(
|
|
117
|
+
UseRequestInterception,
|
|
118
|
+
"useRequestInterception",
|
|
119
|
+
).mockReturnValue(interceptedHandler);
|
|
120
|
+
const trackDataRequestSpy = jest.spyOn(
|
|
121
|
+
RequestTracker.Default,
|
|
122
|
+
"trackDataRequest",
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Act
|
|
126
|
+
serverRenderHook(
|
|
127
|
+
() => useServerEffect("ID", fakeHandler, {skip: true}),
|
|
128
|
+
{
|
|
129
|
+
wrapper: TrackData,
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Assert
|
|
134
|
+
expect(trackDataRequestSpy).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should not track the intercepted request if there is a cached result", () => {
|
|
138
|
+
// Arrange
|
|
139
|
+
const fakeHandler = jest.fn();
|
|
140
|
+
const interceptedHandler = jest.fn();
|
|
141
|
+
jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
|
|
142
|
+
data: "DATA",
|
|
143
|
+
error: null,
|
|
144
|
+
});
|
|
145
|
+
jest.spyOn(
|
|
146
|
+
UseRequestInterception,
|
|
147
|
+
"useRequestInterception",
|
|
148
|
+
).mockReturnValue(interceptedHandler);
|
|
149
|
+
const trackDataRequestSpy = jest.spyOn(
|
|
150
|
+
RequestTracker.Default,
|
|
151
|
+
"trackDataRequest",
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Act
|
|
155
|
+
serverRenderHook(() => useServerEffect("ID", fakeHandler), {
|
|
156
|
+
wrapper: TrackData,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Assert
|
|
160
|
+
expect(trackDataRequestSpy).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
|
|
112
163
|
it("should return data cached result", () => {
|
|
113
164
|
// Arrange
|
|
114
165
|
const fakeHandler = jest.fn();
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
|
|
4
|
-
import {AbortError} from "../util/abort-error.js";
|
|
5
|
-
|
|
6
4
|
import {useServerEffect} from "./use-server-effect.js";
|
|
7
5
|
import {useSharedCache} from "./use-shared-cache.js";
|
|
8
6
|
import {useCachedEffect} from "./use-cached-effect.js";
|
|
@@ -141,16 +139,11 @@ export const useHydratableEffect = <TData: ValidCacheData>(
|
|
|
141
139
|
// Now we instruct the server to perform the operation.
|
|
142
140
|
// When client-side, this will look up any response for hydration; it does
|
|
143
141
|
// not invoke the handler.
|
|
144
|
-
const serverResult = useServerEffect(
|
|
145
|
-
requestId,
|
|
146
|
-
|
|
147
|
-
// If we're skipped (unlikely in server worlds, but maybe),
|
|
148
|
-
// just give an aborted response.
|
|
149
|
-
skip ? () => Promise.reject(new AbortError("skipped")) : handler,
|
|
150
|
-
|
|
142
|
+
const serverResult = useServerEffect(requestId, handler, {
|
|
151
143
|
// Only hydrate if our behavior isn't telling us not to.
|
|
152
|
-
clientBehavior !== WhenClientSide.DoNotHydrate,
|
|
153
|
-
|
|
144
|
+
hydrate: clientBehavior !== WhenClientSide.DoNotHydrate,
|
|
145
|
+
skip,
|
|
146
|
+
});
|
|
154
147
|
|
|
155
148
|
const getDefaultCacheValue: () => ?Result<TData> = React.useCallback(() => {
|
|
156
149
|
// If we don't have a requestId, it's our first render, the one
|
|
@@ -178,9 +171,16 @@ export const useHydratableEffect = <TData: ValidCacheData>(
|
|
|
178
171
|
}
|
|
179
172
|
return null;
|
|
180
173
|
}
|
|
181
|
-
// There is no reason for this to change after the first render
|
|
174
|
+
// There is no reason for this to change after the first render,
|
|
175
|
+
// you might think, but the function closes around serverResult and if
|
|
176
|
+
// the requestId changes, it still returns the hydrate result of the
|
|
177
|
+
// first render of the previous requestId. This then means that the
|
|
178
|
+
// hydrate result is still the same, and the effect is not re-executed
|
|
179
|
+
// because the cache gets incorrectly defaulted.
|
|
180
|
+
// However, we don't want to bother doing anything with this on
|
|
181
|
+
// client behavior changing since that truly is irrelevant.
|
|
182
182
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
183
|
-
}, []);
|
|
183
|
+
}, [serverResult]);
|
|
184
184
|
|
|
185
185
|
// Instead of using state, which would be local to just this hook instance,
|
|
186
186
|
// we use a shared in-memory cache.
|
|
@@ -8,6 +8,29 @@ import {useRequestInterception} from "./use-request-interception.js";
|
|
|
8
8
|
|
|
9
9
|
import type {Result, ValidCacheData} from "../util/types.js";
|
|
10
10
|
|
|
11
|
+
type ServerEffectOptions = {|
|
|
12
|
+
/**
|
|
13
|
+
* When `true`, the result of the effect when fulfilled using Wonder Blocks
|
|
14
|
+
* Data will be stored in the hydration cache for hydrating client-side;
|
|
15
|
+
* otherwise, the result will be stored in the server-side-only cache.
|
|
16
|
+
*
|
|
17
|
+
* This should only be set to `false` if something else will be responsible
|
|
18
|
+
* for hydration of the data on the client-side (for example, if Apollo's
|
|
19
|
+
* hydration support is used).
|
|
20
|
+
*
|
|
21
|
+
* Default is `true`.
|
|
22
|
+
*/
|
|
23
|
+
hydrate?: boolean,
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* When `true`, the effect will not be tracked for fulfillment; otherwise,
|
|
27
|
+
* the effect will be tracked for fulfillment.
|
|
28
|
+
*
|
|
29
|
+
* Default is `false`.
|
|
30
|
+
*/
|
|
31
|
+
skip?: boolean,
|
|
32
|
+
|};
|
|
33
|
+
|
|
11
34
|
/**
|
|
12
35
|
* Hook to perform an asynchronous action during server-side rendering.
|
|
13
36
|
*
|
|
@@ -26,8 +49,10 @@ import type {Result, ValidCacheData} from "../util/types.js";
|
|
|
26
49
|
export const useServerEffect = <TData: ValidCacheData>(
|
|
27
50
|
requestId: string,
|
|
28
51
|
handler: () => Promise<TData>,
|
|
29
|
-
|
|
52
|
+
options: ServerEffectOptions = ({}: $Shape<ServerEffectOptions>),
|
|
30
53
|
): ?Result<TData> => {
|
|
54
|
+
const {hydrate = true, skip = false} = options;
|
|
55
|
+
|
|
31
56
|
// Plug in to the request interception framework for code that wants
|
|
32
57
|
// to use that.
|
|
33
58
|
const interceptedHandler = useRequestInterception(requestId, handler);
|
|
@@ -39,11 +64,11 @@ export const useServerEffect = <TData: ValidCacheData>(
|
|
|
39
64
|
// initial render - and subsequent renders on the client it will be null.
|
|
40
65
|
const cachedResult = SsrCache.Default.getEntry<TData>(requestId);
|
|
41
66
|
|
|
42
|
-
// We only track data requests when we are server-side
|
|
43
|
-
// already have a result, as given by the
|
|
44
|
-
// initial value for the result state).
|
|
67
|
+
// We only track data requests when we are server-side, we are not skipping
|
|
68
|
+
// the request, and we don't already have a result, as given by the
|
|
69
|
+
// cachedData (which is also the initial value for the result state).
|
|
45
70
|
const maybeTrack = useContext(TrackerContext);
|
|
46
|
-
if (cachedResult == null && Server.isServerSide()) {
|
|
71
|
+
if (!skip && cachedResult == null && Server.isServerSide()) {
|
|
47
72
|
maybeTrack?.(requestId, interceptedHandler, hydrate);
|
|
48
73
|
}
|
|
49
74
|
|
package/src/util/abort-error.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Simple implementation to represent aborting.
|
|
5
|
-
*
|
|
6
|
-
* Other frameworks may provide this too, so we won't be sharing this with
|
|
7
|
-
* the outside world. It's just a utility for test and internal use whenever
|
|
8
|
-
* we need to represent the concept of aborted things.
|
|
9
|
-
*/
|
|
10
|
-
export class AbortError extends Error {
|
|
11
|
-
constructor(message: string) {
|
|
12
|
-
super(message);
|
|
13
|
-
this.name = "AbortError";
|
|
14
|
-
}
|
|
15
|
-
}
|