@khanacademy/wonder-blocks-data 7.0.1 → 8.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 +20 -0
- package/dist/es/index.js +284 -100
- package/dist/index.js +1180 -800
- package/package.json +1 -1
- package/src/__docs__/_overview_ssr_.stories.mdx +13 -13
- package/src/__docs__/exports.abort-inflight-requests.stories.mdx +20 -0
- package/src/__docs__/exports.data.stories.mdx +3 -3
- package/src/__docs__/{exports.fulfill-all-data-requests.stories.mdx → exports.fetch-tracked-requests.stories.mdx} +5 -5
- package/src/__docs__/exports.get-gql-request-id.stories.mdx +24 -0
- package/src/__docs__/{exports.has-unfulfilled-requests.stories.mdx → exports.has-tracked-requests-to-be-fetched.stories.mdx} +4 -4
- package/src/__docs__/exports.intialize-hydration-cache.stories.mdx +29 -0
- package/src/__docs__/exports.purge-caches.stories.mdx +23 -0
- package/src/__docs__/{exports.remove-all-from-cache.stories.mdx → exports.purge-hydration-cache.stories.mdx} +4 -4
- package/src/__docs__/{exports.clear-shared-cache.stories.mdx → exports.purge-shared-cache.stories.mdx} +4 -4
- package/src/__docs__/exports.track-data.stories.mdx +4 -4
- package/src/__docs__/exports.use-cached-effect.stories.mdx +7 -4
- package/src/__docs__/exports.use-gql.stories.mdx +1 -33
- package/src/__docs__/exports.use-server-effect.stories.mdx +1 -1
- package/src/__docs__/exports.use-shared-cache.stories.mdx +2 -2
- package/src/__docs__/types.fetch-policy.stories.mdx +44 -0
- package/src/__docs__/types.response-cache.stories.mdx +1 -1
- package/src/__tests__/generated-snapshot.test.js +5 -5
- package/src/components/__tests__/data.test.js +2 -6
- package/src/hooks/__tests__/use-cached-effect.test.js +341 -100
- package/src/hooks/__tests__/use-hydratable-effect.test.js +15 -9
- package/src/hooks/__tests__/use-shared-cache.test.js +6 -6
- package/src/hooks/use-cached-effect.js +169 -93
- package/src/hooks/use-hydratable-effect.js +8 -1
- package/src/hooks/use-shared-cache.js +2 -2
- package/src/index.js +14 -78
- package/src/util/__tests__/get-gql-request-id.test.js +74 -0
- package/src/util/__tests__/graphql-document-node-parser.test.js +542 -0
- package/src/util/__tests__/hydration-cache-api.test.js +35 -0
- package/src/util/__tests__/purge-caches.test.js +29 -0
- package/src/util/__tests__/request-api.test.js +188 -0
- package/src/util/__tests__/request-fulfillment.test.js +42 -0
- package/src/util/__tests__/ssr-cache.test.js +10 -60
- package/src/util/__tests__/to-gql-operation.test.js +42 -0
- package/src/util/data-error.js +6 -0
- package/src/util/get-gql-request-id.js +50 -0
- package/src/util/graphql-document-node-parser.js +133 -0
- package/src/util/graphql-types.js +30 -0
- package/src/util/hydration-cache-api.js +28 -0
- package/src/util/purge-caches.js +15 -0
- package/src/util/request-api.js +66 -0
- package/src/util/request-fulfillment.js +32 -12
- package/src/util/request-tracking.js +1 -1
- package/src/util/ssr-cache.js +1 -21
- package/src/util/to-gql-operation.js +44 -0
- package/src/util/types.js +31 -0
- package/src/__docs__/exports.intialize-cache.stories.mdx +0 -29
- package/src/__docs__/exports.remove-from-cache.stories.mdx +0 -25
- package/src/__docs__/exports.request-fulfillment.stories.mdx +0 -36
package/package.json
CHANGED
|
@@ -56,11 +56,11 @@ renderToString(trackedElement);
|
|
|
56
56
|
|
|
57
57
|
After each render, we want to see if there are any tracked requests needing to be fulfilled. If there are, we will want to fulfill them and render again; if there are not, we are ready to finish the page rendering and move on.
|
|
58
58
|
|
|
59
|
-
To determine which path to take, we can use [`
|
|
59
|
+
To determine which path to take, we can use [`hasTrackedRequestsToBeFetched`](/docs/data-exports-hastrackedrequeststobefetched--page), and then based off that result, either fulfill the requests, or finish rendering our page.
|
|
60
60
|
|
|
61
61
|
```ts
|
|
62
|
-
if (
|
|
63
|
-
await
|
|
62
|
+
if (hasTrackedRequestsToBeFetched()) {
|
|
63
|
+
await fetchTrackedRequests();
|
|
64
64
|
|
|
65
65
|
// Render again.
|
|
66
66
|
// ...
|
|
@@ -72,7 +72,7 @@ if (hasUnfulfilledRequests()) {
|
|
|
72
72
|
|
|
73
73
|
Internally, Wonder Blocks Data caches the responses of fulfilled requests and does not track them again. Each cycle of the rendering should occur with the same knowledge you expect the client-side to have when it performs the render, so although we want to retain our hydration cache response, we need to make sure any transient caches are cleared.
|
|
74
74
|
|
|
75
|
-
Once the rendered component no longer requires any further data, the cached responses can be obtained to include in the rendered page for the client to hydrate. The hydration cache is promised by `
|
|
75
|
+
Once the rendered component no longer requires any further data, the cached responses can be obtained to include in the rendered page for the client to hydrate. The hydration cache is promised by `fetchTrackedRequests()`, regardless of whether it actually had any pending requests or not.
|
|
76
76
|
|
|
77
77
|
### Putting it all together
|
|
78
78
|
|
|
@@ -84,9 +84,9 @@ A function for server-side rendering with Wonder Blocks Data could look somethin
|
|
|
84
84
|
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
85
85
|
import {
|
|
86
86
|
TrackData,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
hasTrackedRequestsToBeFetched,
|
|
88
|
+
fetchTrackedRequests,
|
|
89
|
+
purgeSharedCache,
|
|
90
90
|
} from "@khanacademy/wonder-blocks-data";
|
|
91
91
|
|
|
92
92
|
// Don't forget to import your app!
|
|
@@ -113,14 +113,14 @@ async function renderApp(): Promise<string> {
|
|
|
113
113
|
* shared cache used by the `useSharedCache` hook as this is transient
|
|
114
114
|
* cache that does not itself get directly hydrated.
|
|
115
115
|
*/
|
|
116
|
-
|
|
116
|
+
purgeSharedCache();
|
|
117
117
|
|
|
118
118
|
// Render the tracked component.
|
|
119
119
|
renderedComponent = renderToString(trackedElement);
|
|
120
120
|
|
|
121
|
-
if (
|
|
121
|
+
if (hasTrackedRequestsToBeFetched()) {
|
|
122
122
|
// Fulfill any pending requests.
|
|
123
|
-
await
|
|
123
|
+
await fetchTrackedRequests();
|
|
124
124
|
|
|
125
125
|
// We can't leave the loop yet as we want to render the element
|
|
126
126
|
// again with the newly fulfilled data.
|
|
@@ -130,7 +130,7 @@ async function renderApp(): Promise<string> {
|
|
|
130
130
|
|
|
131
131
|
// We rendered with all the data fulfilled, so we can grab the
|
|
132
132
|
// hydration cache contents and exit the loop now.
|
|
133
|
-
hydrationCache = await
|
|
133
|
+
hydrationCache = await fetchTrackedRequests();
|
|
134
134
|
} while (hydrationCache == null);
|
|
135
135
|
|
|
136
136
|
// Finally, render the page with our hydration cache and rendered
|
|
@@ -164,12 +164,12 @@ In the previous section, this was displayed as the `hydrate.js` script. Here is
|
|
|
164
164
|
|
|
165
165
|
```tsx
|
|
166
166
|
import {hydrate} from "react-dom";
|
|
167
|
-
import {
|
|
167
|
+
import {initializeHydrationCache} from "@khanacademy/wonder-blocks-data";
|
|
168
168
|
|
|
169
169
|
// Don't forget to import your app!
|
|
170
170
|
import App from "./App.js";
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
initializeHydrationCache(window._WONDER_BLOCKS_DATA_);
|
|
173
173
|
|
|
174
174
|
React.hydrate(
|
|
175
175
|
// This should match whatever was passed to the server-side rendering
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Exports / abortInflightRequests()"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# abortInflightRequests()
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
abortInflightRequests(): void;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Aborts all inflight requests that were started via [`useCachedEffect`](/docs/data-exports-usecachedeffect--page), [`useHydratableEffect`](/docs/data-exports-usehydratableeffect--page), or [`fetchTrackedRequests()`](/docs/data-exports-fetchtrackedrequests--page).
|
|
19
|
+
|
|
20
|
+
NOTE: Full abort signalling is not currently implemented. The effect of this call is to remove any inflight requests from our inflight request tracking such that a new matching request will cause a new fetch to occur rather than sharing any existing one.
|
|
@@ -105,12 +105,12 @@ const myInvalidHandler = () => new Promise((resolve, reject) =>
|
|
|
105
105
|
|
|
106
106
|
If the hydration cache already contains data or an error for our request, then
|
|
107
107
|
the `Data` component will render it immediately. The hydration cache is
|
|
108
|
-
populated using the `
|
|
108
|
+
populated using the `initializeHydrationCache` method before rendering.
|
|
109
109
|
|
|
110
110
|
```jsx
|
|
111
111
|
import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
|
|
112
112
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
113
|
-
import {Data,
|
|
113
|
+
import {Data, initializeHydrationCache} from "@khanacademy/wonder-blocks-data";
|
|
114
114
|
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
115
115
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
116
116
|
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
@@ -121,7 +121,7 @@ const myHandler = () => {
|
|
|
121
121
|
);
|
|
122
122
|
};
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
initializeHydrationCache({
|
|
125
125
|
DATA: {
|
|
126
126
|
data: "I'm DATA from the hydration cache"
|
|
127
127
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {Meta} from "@storybook/addon-docs";
|
|
2
2
|
|
|
3
3
|
<Meta
|
|
4
|
-
title="Data / Exports /
|
|
4
|
+
title="Data / Exports / fetchTrackedRequests()"
|
|
5
5
|
parameters={{
|
|
6
6
|
chromatic: {
|
|
7
7
|
disableSnapshot: true,
|
|
@@ -9,16 +9,16 @@ import {Meta} from "@storybook/addon-docs";
|
|
|
9
9
|
}}
|
|
10
10
|
/>
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# fetchTrackedRequests()
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
|
-
|
|
15
|
+
fetchTrackedRequests(): Promise<ResponseCache>;
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
When performing server-side rendering (SSR), the data requests that are being made via the [`Data`](/docs/data-exports-data--page) component can be tracked by rendering the React tree inside the [`TrackData`](/docs/data-exports-trackdata--page) component. After this has occurred, the tracked requests can be fulfilled using `
|
|
18
|
+
When performing server-side rendering (SSR), the data requests that are being made via the [`Data`](/docs/data-exports-data--page) component can be tracked by rendering the React tree inside the [`TrackData`](/docs/data-exports-trackdata--page) component. After this has occurred, the tracked requests can be fulfilled using `fetchTrackedRequests`.
|
|
19
19
|
|
|
20
20
|
This method returns a promise that resolves to a copy of the data that was cached by fulfilling the tracked requests. In the process, it clears the record of tracked requests so that new requests can be tracked and fulfilled if so required.
|
|
21
21
|
|
|
22
|
-
The returned copy of the data cache can be used with the [`
|
|
22
|
+
The returned copy of the data cache can be used with the [`initializeHydrationCache`](/docs/data-exports-initializehydrationcache--page) method to prepare the data cache before a subsequent render. This is useful on the server to then SSR a more complete result, and again on the client, to rehydrate that result.
|
|
23
23
|
|
|
24
24
|
More details about server-side rendering with Wonder Blocks Data can be found in the [relevant overview section](/docs/data-server-side-rendering-and-hydration--page).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Exports / getGqlRequestId()"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# getGqlRequestId()
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
function getGqlRequestId<TData, TVariables: {...}>(
|
|
16
|
+
operation: GqlOperation<TData, TVariables>,
|
|
17
|
+
variables: ?TVariables,
|
|
18
|
+
context: GqlContext,
|
|
19
|
+
): string;
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The `getGqlRequestId` function generates an identifier based on the operation, variables, and context of a specific GraphQL request. This identifier is guaranteed to be the same for all requests that share the same operation, variables, and context, even if variables and context values are in different orders.
|
|
23
|
+
|
|
24
|
+
The identifier returned by this function can then be used with our [`useCachedEffect`](/docs/data-exports-usecachedeffect--page), [`useServerEffect`](/docs/data-exports-useservereffect--page), and [`useHydratableEffect`](/docs/data-exports-usehydratableeffect--page) hooks as the `requestId` parameter, allowing them to be combined with the fetch operation from the [`useGql`](/docs/data-exports-usegql--page) hook.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {Meta} from "@storybook/addon-docs";
|
|
2
2
|
|
|
3
3
|
<Meta
|
|
4
|
-
title="Data / Exports /
|
|
4
|
+
title="Data / Exports / hasTrackedRequestsToBeFetched()"
|
|
5
5
|
parameters={{
|
|
6
6
|
chromatic: {
|
|
7
7
|
disableSnapshot: true,
|
|
@@ -9,12 +9,12 @@ import {Meta} from "@storybook/addon-docs";
|
|
|
9
9
|
}}
|
|
10
10
|
/>
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# hasTrackedRequestsToBeFetched()
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
|
-
|
|
15
|
+
hasTrackedRequestsToBeFetched(): boolean;
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
When performing server-side rendering (SSR), any requests that have been tracked will cause this method to return `true`. Once [`
|
|
18
|
+
When performing server-side rendering (SSR), any requests that have been tracked will cause this method to return `true`. Once [`fetchTrackedRequests`](/docs/data-exports-fetchtrackedrequests--page) has been called and the promise is settled, this method will return `false` to indicate that there are no more pending requests.
|
|
19
19
|
|
|
20
20
|
More details about server-side rendering with Wonder Blocks Data can be found in the [relevant overview section](/docs/data-server-side-rendering-and-hydration--page).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Exports / initializeHydrationCache()"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# initializeHydrationCache()
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
initializeHydrationCache(sourceCache: ResponseCache): void;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Argument | Flow Type | Default | Description |
|
|
19
|
+
| --- | --- | --- | --- |
|
|
20
|
+
| `sourceData` | `ResponseCache` | _Required_ | The source cache that will be used to initialize the response cache. |
|
|
21
|
+
|
|
22
|
+
Wonder Blocks Data caches data in its response cache for hydration. This cache can be initialized with data using the `initializeHydrationCache` method.
|
|
23
|
+
The `initializeHydrationCache` method can only be called when the hydration cache is empty.
|
|
24
|
+
|
|
25
|
+
Usually, the data to be passed to `v` will be obtained by calling [`fetchTrackedRequests`](/docs/data-exports-fetchtrackedrequests--page) after tracking data requests (see [`TrackData`](/docs/data-exports-trackdata--page)) during server-side rendering.
|
|
26
|
+
|
|
27
|
+
Combine with [`purgeHydrationCache`](/docs/data-exports-purgehydrationcache--page) to support your testing needs.
|
|
28
|
+
|
|
29
|
+
More details about server-side rendering with Wonder Blocks Data can be found in the [relevant overview section](/docs/data-server-side-rendering-and-hydration--page).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Exports / purgeCaches()"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# purgeCaches()
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
purgeCaches(scope?: string): void;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The `purgeCaches` method will purge the following caches managed by Wonder Blocks Data:
|
|
19
|
+
|
|
20
|
+
- Shared in-memory cache as used by [`useSharedCache`](/docs/data-exports-usesharedcache--page) and other hooks
|
|
21
|
+
- Hydration cache as used during server-side rendering
|
|
22
|
+
|
|
23
|
+
This is equivalent to calling both `purgeSharedCache()` and `purgeHydrationCache()`, and is especially useful when writing tests or setting up a test environment.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {Meta} from "@storybook/addon-docs";
|
|
2
2
|
|
|
3
3
|
<Meta
|
|
4
|
-
title="Data / Exports /
|
|
4
|
+
title="Data / Exports / purgeHydrationCache()"
|
|
5
5
|
parameters={{
|
|
6
6
|
chromatic: {
|
|
7
7
|
disableSnapshot: true,
|
|
@@ -9,10 +9,10 @@ import {Meta} from "@storybook/addon-docs";
|
|
|
9
9
|
}}
|
|
10
10
|
/>
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# purgeHydrationCache()
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
|
-
|
|
15
|
+
purgeHydrationCache(predicate?: (key: string, cacheEntry: $ReadOnly<CachedResponse<ValidCacheData>>) => boolean): void;
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
| Argument | Flow Type | Default | Description |
|
|
@@ -21,4 +21,4 @@ removeAllFromCache(predicate?: (key: string, cacheEntry: $ReadOnly<CachedRespons
|
|
|
21
21
|
|
|
22
22
|
Removes all entries that match a given predicate from the cache. If no predicate is given, all cached entries.
|
|
23
23
|
|
|
24
|
-
This can be used after [`
|
|
24
|
+
This can be used after [`initializeHydrationCache`](/docs/data-exports-initializehydrationcache--page) to manipulate the cache prior to hydration wich can be useful during testing (especially to clear the cache so that it can be initialized again).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {Meta} from "@storybook/addon-docs";
|
|
2
2
|
|
|
3
3
|
<Meta
|
|
4
|
-
title="Data / Exports /
|
|
4
|
+
title="Data / Exports / purgeSharedCache()"
|
|
5
5
|
parameters={{
|
|
6
6
|
chromatic: {
|
|
7
7
|
disableSnapshot: true,
|
|
@@ -9,12 +9,12 @@ import {Meta} from "@storybook/addon-docs";
|
|
|
9
9
|
}}
|
|
10
10
|
/>
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# purgeSharedCache()
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
|
-
|
|
15
|
+
purgeSharedCache(scope?: string): void;
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
The `
|
|
18
|
+
The `purgeSharedCache` method can be used to clear the shared in-memory cache used by the [`useSharedCache`](/docs/data-exports-usesharedcache--page) hook. Either a single scope or all scopes can be cleared.
|
|
19
19
|
|
|
20
20
|
Common uses for calling this method are during [server-side rendering](/docs/data-server-side-rendering-and-hydration--page) to ensure each render cycle remains isolated, or during testing in a `beforeEach` to cover for where previous test cases may have changed the shared cache.
|
|
@@ -71,7 +71,7 @@ class ErrorBoundary extends React.Component {
|
|
|
71
71
|
|
|
72
72
|
When used server-side, this component tracks any data requests made through
|
|
73
73
|
the `Data` component during a render cycle. This data can then be obtained
|
|
74
|
-
using the `
|
|
74
|
+
using the `fetchTrackedRequests` method. The data can then be used in an
|
|
75
75
|
additional render cycle to render with that data.
|
|
76
76
|
|
|
77
77
|
```jsx
|
|
@@ -80,7 +80,7 @@ import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
|
80
80
|
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
81
81
|
import Button from "@khanacademy/wonder-blocks-button";
|
|
82
82
|
import {Server, View} from "@khanacademy/wonder-blocks-core";
|
|
83
|
-
import {Data, TrackData,
|
|
83
|
+
import {Data, TrackData, fetchTrackedRequests} from "@khanacademy/wonder-blocks-data";
|
|
84
84
|
|
|
85
85
|
const myPretendHandler = () => new Promise((resolve, reject) =>
|
|
86
86
|
setTimeout(() => resolve("DATA!"), 3000),
|
|
@@ -158,7 +158,7 @@ class Example extends React.Component {
|
|
|
158
158
|
<Body>
|
|
159
159
|
The above components requested data, but we're server-side,
|
|
160
160
|
so all that happened is we tracked the request.
|
|
161
|
-
In this example, we've also called `
|
|
161
|
+
In this example, we've also called `fetchTrackedRequests`
|
|
162
162
|
to fetch that tracked data.
|
|
163
163
|
</Body>
|
|
164
164
|
<Strut size={Spacing.small_12} />
|
|
@@ -195,7 +195,7 @@ class Example extends React.Component {
|
|
|
195
195
|
);
|
|
196
196
|
} finally {
|
|
197
197
|
if (!this.state.data && Server.isServerSide()) {
|
|
198
|
-
setTimeout(() =>
|
|
198
|
+
setTimeout(() => fetchTrackedRequests().then((data) => {
|
|
199
199
|
if (this._mounted) {
|
|
200
200
|
this.setState({data});
|
|
201
201
|
}
|
|
@@ -16,13 +16,14 @@ function useCachedEffect<TData: ValidCacheData>(
|
|
|
16
16
|
requestId: string,
|
|
17
17
|
handler: () => Promise<TData>,
|
|
18
18
|
options?: CachedEffectOptions<TData>,
|
|
19
|
-
): Result<TData
|
|
19
|
+
): [Result<TData>, () => void];
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
This hook
|
|
23
|
-
hook can be modified using the options.
|
|
22
|
+
This hook invokes the given handler and caches the result using the [`useSharedCache`](/docs/data-exports-usesharedcache--page) hook. The `requestId` is used to both identify inflight requests that can be shared, and to identify the cached value to use.
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
The hook returns an array containing the current state of the request, and a function that can be used to `refetch` that request on demand. Calling `refetch` while an inflight request is in progress for the given `requestId` will be a no-op.
|
|
25
|
+
|
|
26
|
+
The behavior of the hook can be modified with the options.
|
|
26
27
|
|
|
27
28
|
```ts
|
|
28
29
|
type CachedEffectOptions<TData: ValidCacheData> = {|
|
|
@@ -30,6 +31,7 @@ type CachedEffectOptions<TData: ValidCacheData> = {|
|
|
|
30
31
|
retainResultOnChange?: boolean,
|
|
31
32
|
onResultChanged?: (result: Result<TData>) => void,
|
|
32
33
|
scope?: string,
|
|
34
|
+
fetchPolicy?: FetchPolicy,
|
|
33
35
|
|};
|
|
34
36
|
```
|
|
35
37
|
|
|
@@ -39,3 +41,4 @@ type CachedEffectOptions<TData: ValidCacheData> = {|
|
|
|
39
41
|
| `retainResultOnChange` | `false` | When `true`, the effect will not reset the result to the loading status while executing if the requestId changes, instead, returning the existing result from before the change; otherwise, the result will be set to loading status. If the status is loading when the changes are made, it will remain as loading; old pending effects are discarded on changes and as such this value has no effect in that case.|
|
|
40
42
|
| `onResultChanged` | `undefined` | Callback that is invoked if the result for the given hook has changed. When defined, the hook will invoke this callback whenever it has reason to change the result and will not otherwise affect component rendering directly. When not defined, the hook will ensure the component re-renders to pick up the latest result. |
|
|
41
43
|
| `scope` | `"useCachedEffect"` | Scope to use with the shared cache. When specified, the given scope will be used to isolate this hook's cached results. Otherwise, the default scope will be used. Changing this value after the first call is not supported. |
|
|
44
|
+
| `fetchPolicy` | [`FetchPolicy`](/docs/data-types-fetchpolicy--page) | Fetch policy to use when fetching the data. Defaults to `FetchPolicy.CacheBeforeNetwork`. |
|
|
@@ -30,39 +30,7 @@ The return value of `useGql` is a fetch function that can be used to invoke a Gr
|
|
|
30
30
|
|
|
31
31
|
The result of calling the function returned by `useGql` is a promise of the data that the request will return. This is compatible with the [`useServerEffect`](/docs/data-exports-useservereffect--page), [`useCachedEffect`](/docs/data-exports-usecachedeffect--page), and [`useHydratableEffect`](/docs/data-exports-usehydratableeffect--page) hooks, allowing a variety of scenarios to be easily constructed.
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
const getGqlRequestId = <TData, TVariables: {...}>(
|
|
37
|
-
operation: GqlOperation<TData, TVariables>,
|
|
38
|
-
variables: ?TVariables,
|
|
39
|
-
context: GqlContext,
|
|
40
|
-
): string => {
|
|
41
|
-
// We add all the bits for this into an array and then join them with
|
|
42
|
-
// a chosen separator.
|
|
43
|
-
const parts = [];
|
|
44
|
-
parts.push(operation.id);
|
|
45
|
-
if (context != null) {
|
|
46
|
-
// Turn the context into a string of the form,
|
|
47
|
-
// "key1=;key2=value"
|
|
48
|
-
const sortedContext = Object.keys(context || {})
|
|
49
|
-
.sort()
|
|
50
|
-
.map((key) => `${key}=${JSON.stringify(context?.[key]) || ""}`)
|
|
51
|
-
.join(";");
|
|
52
|
-
parts.push(sortedContext);
|
|
53
|
-
}
|
|
54
|
-
if (variables != null) {
|
|
55
|
-
// Turn the variables into a string of the form,
|
|
56
|
-
// "key1=;key2=value"
|
|
57
|
-
const sortedVariables = Object.keys(variables || {})
|
|
58
|
-
.sort()
|
|
59
|
-
.map((key) => `${key}=${JSON.stringify(variables?.[key]) || ""}`)
|
|
60
|
-
.join(";");
|
|
61
|
-
parts.push(sortedVariables);
|
|
62
|
-
}
|
|
63
|
-
return parts.join("|");
|
|
64
|
-
};
|
|
65
|
-
```
|
|
33
|
+
Use [`getGqlRequestId`](/docs/data-exports-getgqlrequestid--page) to get a request ID that can be used with these hooks.
|
|
66
34
|
|
|
67
35
|
## Context Merging
|
|
68
36
|
|
|
@@ -39,7 +39,7 @@ First, this hook checks the server-side rendering cache for the request identifi
|
|
|
39
39
|
|
|
40
40
|
If there is no cached value, it will return a "loading" state. In addition, if the current rendering component has a [`TrackData`](/docs/data-exports-trackdata--page) ancestor, `useServerEffect` will register the request for fulfillment.
|
|
41
41
|
|
|
42
|
-
This then allows that pending request to be fulfilled with [`
|
|
42
|
+
This then allows that pending request to be fulfilled with [`fetchTrackedRequests`](/docs/data-exports-fetchtrackddatarequests--page), the response to be placed into the cache, and the render to be reexecuted, at which point, this hook will be able to provide that result instead of "loading.
|
|
43
43
|
|
|
44
44
|
More details about server-side rendering with Wonder Blocks Data can be found in the [relevant overview section](/docs/data-server-side-rendering-and-hydration--page).
|
|
45
45
|
|
|
@@ -19,12 +19,12 @@ function useSharedCache<TValue: ValidCacheData>(
|
|
|
19
19
|
): [?TValue, CacheValueFn<TValue>];
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
The `useSharedCache` hook provides access to a shared in-memory cache. This cache is not part of the cache hydrated by Wonder Blocks Data, so [`
|
|
22
|
+
The `useSharedCache` hook provides access to a shared in-memory cache. This cache is not part of the cache hydrated by Wonder Blocks Data, so [`purgeSharedCache`](/docs/data-exports-purgesharedcache--page) must be called between server-side render cycles.
|
|
23
23
|
|
|
24
24
|
The hook returns a tuple of the currently cached value, or `null` if none is cached, and a function that can be used to set the cached value.
|
|
25
25
|
|
|
26
26
|
The shared cache is passive and as such does not notify of changes to its contents.
|
|
27
27
|
|
|
28
|
-
Each cached item is identified by an id and a scope. The scope is used to group items. Whole scopes can be cleared by specifying the specific scope when calling [`
|
|
28
|
+
Each cached item is identified by an id and a scope. The scope is used to group items. Whole scopes can be cleared by specifying the specific scope when calling [`purgeSharedCache`](/docs/data-exports-purgesharedcache--page).
|
|
29
29
|
|
|
30
30
|
An optional argument, `initialValue` can be given. This can be either the value to be cached itself or a function that returns the value to be cached (functions themselves are not valid cachable values). This allows for expensive initialization to only occur when it is necessary.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Types / FetchPolicy"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# FetchPolicy
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
export enum FetchPolicy {
|
|
16
|
+
/**
|
|
17
|
+
* If the data is in the cache, return that; otherwise, fetch from the server.
|
|
18
|
+
*/
|
|
19
|
+
CacheBeforeNetwork,
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* If the data is in the cache, return that; always fetch from the server
|
|
23
|
+
* regardless of cache.
|
|
24
|
+
*/
|
|
25
|
+
CacheAndNetwork,
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* If the data is in the cache, return that; otherwise, do nothing.
|
|
29
|
+
*/
|
|
30
|
+
CacheOnly,
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ignore any existing cached result; fetch from the server.
|
|
34
|
+
*/
|
|
35
|
+
NetworkOnly,
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The `FetchPolicy` type is used with our request framework to define how a request should be fulfilled with respect to the cache and the network.
|
|
40
|
+
|
|
41
|
+
* `CacheBeforeNetwork`: If the data is in the cache, return that; otherwise, fetch from the server.
|
|
42
|
+
* `CacheAndNetwork`: If the data is in the cache, return that; always fetch from the server regardless of cache.
|
|
43
|
+
* `CacheOnly`: If the data is in the cache, return that; otherwise, do nothing.
|
|
44
|
+
* `NetworkOnly`: Ignore any existing cached result; always fetch from the server.
|
|
@@ -18,7 +18,7 @@ type ResponseCache = {
|
|
|
18
18
|
};
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
`ResponseCache` describes the serialized cache that is used to hydrate responses. An example of a valid `ResponseCache` instance is shown below. Generally, you would not generate this object directly, but rather use the returned data from [`
|
|
21
|
+
`ResponseCache` describes the serialized cache that is used to hydrate responses. An example of a valid `ResponseCache` instance is shown below. Generally, you would not generate this object directly, but rather use the returned data from [`fetchTrackedRequests`](/docs/data-exports-fetchtrackedrequests--page).
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
const responseCache: ResponseCache = {
|
|
@@ -12,10 +12,10 @@ import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
|
|
|
12
12
|
import {View, Server} from "@khanacademy/wonder-blocks-core";
|
|
13
13
|
import {
|
|
14
14
|
Data,
|
|
15
|
-
|
|
15
|
+
initializeHydrationCache,
|
|
16
16
|
InterceptRequests,
|
|
17
17
|
TrackData,
|
|
18
|
-
|
|
18
|
+
fetchTrackedRequests,
|
|
19
19
|
} from "@khanacademy/wonder-blocks-data";
|
|
20
20
|
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
21
21
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
@@ -82,7 +82,7 @@ describe("wonder-blocks-data", () => {
|
|
|
82
82
|
);
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
initializeHydrationCache({
|
|
86
86
|
DATA: {
|
|
87
87
|
data: "I'm DATA from the hydration cache",
|
|
88
88
|
},
|
|
@@ -277,7 +277,7 @@ describe("wonder-blocks-data", () => {
|
|
|
277
277
|
The above components requested data, but we're
|
|
278
278
|
server-side, so all that happened is we tracked
|
|
279
279
|
the request. In this example, we've also called
|
|
280
|
-
`
|
|
280
|
+
`fetchTrackedRequests` to fetch that tracked
|
|
281
281
|
data.
|
|
282
282
|
</Body>
|
|
283
283
|
<Strut size={Spacing.small_12} />
|
|
@@ -329,7 +329,7 @@ describe("wonder-blocks-data", () => {
|
|
|
329
329
|
if (!this.state.data && Server.isServerSide()) {
|
|
330
330
|
setTimeout(
|
|
331
331
|
() =>
|
|
332
|
-
|
|
332
|
+
fetchTrackedRequests().then((data) => {
|
|
333
333
|
if (this._mounted) {
|
|
334
334
|
this.setState({
|
|
335
335
|
data,
|
|
@@ -7,7 +7,7 @@ import {render, act} from "@testing-library/react";
|
|
|
7
7
|
import * as ReactDOMServer from "react-dom/server";
|
|
8
8
|
import {Server, View} from "@khanacademy/wonder-blocks-core";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {purgeSharedCache} from "../../hooks/use-shared-cache.js";
|
|
11
11
|
import TrackData from "../track-data.js";
|
|
12
12
|
import {RequestFulfillment} from "../../util/request-fulfillment.js";
|
|
13
13
|
import {SsrCache} from "../../util/ssr-cache.js";
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
|
|
25
25
|
describe("Data", () => {
|
|
26
26
|
beforeEach(() => {
|
|
27
|
-
|
|
27
|
+
purgeSharedCache();
|
|
28
28
|
|
|
29
29
|
const responseCache = new SsrCache();
|
|
30
30
|
jest.spyOn(SsrCache, "Default", "get").mockReturnValue(responseCache);
|
|
@@ -36,10 +36,6 @@ describe("Data", () => {
|
|
|
36
36
|
);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
jest.resetAllMocks();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
39
|
describe("CSR: isServerSide false", () => {
|
|
44
40
|
beforeEach(() => {
|
|
45
41
|
jest.spyOn(Server, "isServerSide").mockReturnValue(false);
|