@khanacademy/wonder-blocks-data 6.0.1 → 7.0.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/CHANGELOG.md +6 -0
- package/dist/es/index.js +15 -25
- package/dist/index.js +29 -47
- package/package.json +1 -1
- package/src/__docs__/exports.use-server-effect.stories.mdx +13 -1
- package/src/hooks/__tests__/use-hydratable-effect.test.js +2 -25
- package/src/hooks/__tests__/use-server-effect.test.js +51 -0
- package/src/hooks/use-hydratable-effect.js +4 -11
- package/src/hooks/use-server-effect.js +30 -5
- package/src/util/abort-error.js +0 -15
package/CHANGELOG.md
CHANGED
package/dist/es/index.js
CHANGED
|
@@ -600,21 +600,6 @@ class TrackData extends React.Component {
|
|
|
600
600
|
|
|
601
601
|
}
|
|
602
602
|
|
|
603
|
-
/**
|
|
604
|
-
* Simple implementation to represent aborting.
|
|
605
|
-
*
|
|
606
|
-
* Other frameworks may provide this too, so we won't be sharing this with
|
|
607
|
-
* the outside world. It's just a utility for test and internal use whenever
|
|
608
|
-
* we need to represent the concept of aborted things.
|
|
609
|
-
*/
|
|
610
|
-
class AbortError extends Error {
|
|
611
|
-
constructor(message) {
|
|
612
|
-
super(message);
|
|
613
|
-
this.name = "AbortError";
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
|
|
618
603
|
const loadingStatus = Object.freeze({
|
|
619
604
|
status: "loading"
|
|
620
605
|
});
|
|
@@ -728,22 +713,26 @@ const useRequestInterception = (requestId, handler) => {
|
|
|
728
713
|
*
|
|
729
714
|
* The asynchronous action is never invoked on the client-side.
|
|
730
715
|
*/
|
|
731
|
-
const useServerEffect = (requestId, handler,
|
|
732
|
-
|
|
716
|
+
const useServerEffect = (requestId, handler, options = {}) => {
|
|
717
|
+
const {
|
|
718
|
+
hydrate = true,
|
|
719
|
+
skip = false
|
|
720
|
+
} = options; // Plug in to the request interception framework for code that wants
|
|
733
721
|
// to use that.
|
|
722
|
+
|
|
734
723
|
const interceptedHandler = useRequestInterception(requestId, handler); // If we're server-side or hydrating, we'll have a cached entry to use.
|
|
735
724
|
// So we get that and use it to initialize our state.
|
|
736
725
|
// This works in both hydration and SSR because the very first call to
|
|
737
726
|
// this will have cached data in those cases as it will be present on the
|
|
738
727
|
// initial render - and subsequent renders on the client it will be null.
|
|
739
728
|
|
|
740
|
-
const cachedResult = SsrCache.Default.getEntry(requestId); // We only track data requests when we are server-side
|
|
741
|
-
// already have a result, as given by the
|
|
742
|
-
// initial value for the result state).
|
|
729
|
+
const cachedResult = SsrCache.Default.getEntry(requestId); // We only track data requests when we are server-side, we are not skipping
|
|
730
|
+
// the request, and we don't already have a result, as given by the
|
|
731
|
+
// cachedData (which is also the initial value for the result state).
|
|
743
732
|
|
|
744
733
|
const maybeTrack = useContext(TrackerContext);
|
|
745
734
|
|
|
746
|
-
if (cachedResult == null && Server.isServerSide()) {
|
|
735
|
+
if (!skip && cachedResult == null && Server.isServerSide()) {
|
|
747
736
|
maybeTrack == null ? void 0 : maybeTrack(requestId, interceptedHandler, hydrate);
|
|
748
737
|
} // A null result means there was no result to hydrate.
|
|
749
738
|
|
|
@@ -992,10 +981,11 @@ const useHydratableEffect = (requestId, handler, options = {}) => {
|
|
|
992
981
|
// When client-side, this will look up any response for hydration; it does
|
|
993
982
|
// not invoke the handler.
|
|
994
983
|
|
|
995
|
-
const serverResult = useServerEffect(requestId,
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
984
|
+
const serverResult = useServerEffect(requestId, handler, {
|
|
985
|
+
// Only hydrate if our behavior isn't telling us not to.
|
|
986
|
+
hydrate: clientBehavior !== WhenClientSide.DoNotHydrate,
|
|
987
|
+
skip
|
|
988
|
+
});
|
|
999
989
|
const getDefaultCacheValue = React.useCallback(() => {
|
|
1000
990
|
// If we don't have a requestId, it's our first render, the one
|
|
1001
991
|
// where we hydrated. So defer to our clientBehavior value.
|
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.
|
|
@@ -965,11 +964,11 @@ const useHydratableEffect = (requestId, handler, options = {}) => {
|
|
|
965
964
|
}, [serverResult]); // Instead of using state, which would be local to just this hook instance,
|
|
966
965
|
// we use a shared in-memory cache.
|
|
967
966
|
|
|
968
|
-
Object(
|
|
967
|
+
Object(_use_shared_cache_js__WEBPACK_IMPORTED_MODULE_2__[/* useSharedCache */ "b"])(requestId, // The key of the cached item
|
|
969
968
|
scope, // The scope of the cached items
|
|
970
969
|
getDefaultCacheValue); // When we're client-side, we ultimately want the result from this call.
|
|
971
970
|
|
|
972
|
-
const clientResult = Object(
|
|
971
|
+
const clientResult = Object(_use_cached_effect_js__WEBPACK_IMPORTED_MODULE_3__[/* useCachedEffect */ "a"])(requestId, handler, {
|
|
973
972
|
skip,
|
|
974
973
|
onResultChanged,
|
|
975
974
|
retainResultOnChange,
|
|
@@ -1061,7 +1060,7 @@ const InterceptContext = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["create
|
|
|
1061
1060
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
|
|
1062
1061
|
/* harmony import */ var _util_request_tracking_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(7);
|
|
1063
1062
|
/* harmony import */ var _util_ssr_cache_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5);
|
|
1064
|
-
/* 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);
|
|
1065
1064
|
/* harmony import */ var _use_request_interception_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(16);
|
|
1066
1065
|
|
|
1067
1066
|
|
|
@@ -1085,22 +1084,26 @@ const InterceptContext = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["create
|
|
|
1085
1084
|
*
|
|
1086
1085
|
* The asynchronous action is never invoked on the client-side.
|
|
1087
1086
|
*/
|
|
1088
|
-
const useServerEffect = (requestId, handler,
|
|
1089
|
-
|
|
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
|
|
1090
1092
|
// to use that.
|
|
1093
|
+
|
|
1091
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.
|
|
1092
1095
|
// So we get that and use it to initialize our state.
|
|
1093
1096
|
// This works in both hydration and SSR because the very first call to
|
|
1094
1097
|
// this will have cached data in those cases as it will be present on the
|
|
1095
1098
|
// initial render - and subsequent renders on the client it will be null.
|
|
1096
1099
|
|
|
1097
|
-
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
|
|
1098
|
-
// already have a result, as given by the
|
|
1099
|
-
// 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).
|
|
1100
1103
|
|
|
1101
1104
|
const maybeTrack = Object(react__WEBPACK_IMPORTED_MODULE_1__["useContext"])(_util_request_tracking_js__WEBPACK_IMPORTED_MODULE_2__[/* TrackerContext */ "b"]);
|
|
1102
1105
|
|
|
1103
|
-
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()) {
|
|
1104
1107
|
maybeTrack == null ? void 0 : maybeTrack(requestId, interceptedHandler, hydrate);
|
|
1105
1108
|
} // A null result means there was no result to hydrate.
|
|
1106
1109
|
|
|
@@ -1517,8 +1520,8 @@ const GqlRouter = ({
|
|
|
1517
1520
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
|
|
1518
1521
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
|
1519
1522
|
/* harmony import */ var _util_merge_gql_context_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(18);
|
|
1520
|
-
/* harmony import */ var _use_gql_router_context_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(
|
|
1521
|
-
/* 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);
|
|
1522
1525
|
|
|
1523
1526
|
|
|
1524
1527
|
|
|
@@ -1562,27 +1565,6 @@ const useGql = (context = {}) => {
|
|
|
1562
1565
|
/* 24 */
|
|
1563
1566
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1564
1567
|
|
|
1565
|
-
"use strict";
|
|
1566
|
-
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return AbortError; });
|
|
1567
|
-
/**
|
|
1568
|
-
* Simple implementation to represent aborting.
|
|
1569
|
-
*
|
|
1570
|
-
* Other frameworks may provide this too, so we won't be sharing this with
|
|
1571
|
-
* the outside world. It's just a utility for test and internal use whenever
|
|
1572
|
-
* we need to represent the concept of aborted things.
|
|
1573
|
-
*/
|
|
1574
|
-
class AbortError extends Error {
|
|
1575
|
-
constructor(message) {
|
|
1576
|
-
super(message);
|
|
1577
|
-
this.name = "AbortError";
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
/***/ }),
|
|
1583
|
-
/* 25 */
|
|
1584
|
-
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1585
|
-
|
|
1586
1568
|
"use strict";
|
|
1587
1569
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return resultFromCachedResponse; });
|
|
1588
1570
|
/* harmony import */ var _status_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4);
|
|
@@ -1620,7 +1602,7 @@ const resultFromCachedResponse = cacheEntry => {
|
|
|
1620
1602
|
};
|
|
1621
1603
|
|
|
1622
1604
|
/***/ }),
|
|
1623
|
-
/*
|
|
1605
|
+
/* 25 */
|
|
1624
1606
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1625
1607
|
|
|
1626
1608
|
"use strict";
|
|
@@ -1672,7 +1654,7 @@ const useGqlRouterContext = (contextOverrides = {}) => {
|
|
|
1672
1654
|
};
|
|
1673
1655
|
|
|
1674
1656
|
/***/ }),
|
|
1675
|
-
/*
|
|
1657
|
+
/* 26 */
|
|
1676
1658
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1677
1659
|
|
|
1678
1660
|
"use strict";
|
|
@@ -1742,7 +1724,7 @@ const getGqlDataFromResponse = async response => {
|
|
|
1742
1724
|
};
|
|
1743
1725
|
|
|
1744
1726
|
/***/ }),
|
|
1745
|
-
/*
|
|
1727
|
+
/* 27 */
|
|
1746
1728
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1747
1729
|
|
|
1748
1730
|
"use strict";
|
|
@@ -1889,7 +1871,7 @@ const removeAllFromCache = predicate => _util_ssr_cache_js__WEBPACK_IMPORTED_MOD
|
|
|
1889
1871
|
|
|
1890
1872
|
|
|
1891
1873
|
/***/ }),
|
|
1892
|
-
/*
|
|
1874
|
+
/* 28 */
|
|
1893
1875
|
/***/ (function(module, exports, __webpack_require__) {
|
|
1894
1876
|
|
|
1895
1877
|
"use strict";
|
package/package.json
CHANGED
|
@@ -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
|
);
|
|
@@ -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
|
|
@@ -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
|
-
}
|