@khanacademy/wonder-blocks-data 5.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 +31 -0
- package/dist/es/index.js +767 -371
- package/dist/index.js +1194 -564
- package/legacy-docs.md +3 -0
- package/package.json +2 -2
- package/src/__docs__/_overview_.stories.mdx +18 -0
- package/src/__docs__/_overview_graphql.stories.mdx +35 -0
- package/src/__docs__/_overview_ssr_.stories.mdx +185 -0
- package/src/__docs__/_overview_testing_.stories.mdx +123 -0
- package/src/__docs__/exports.clear-shared-cache.stories.mdx +20 -0
- package/src/__docs__/exports.data-error.stories.mdx +23 -0
- package/src/__docs__/exports.data-errors.stories.mdx +23 -0
- package/src/{components/data.md → __docs__/exports.data.stories.mdx} +15 -18
- package/src/__docs__/exports.fulfill-all-data-requests.stories.mdx +24 -0
- package/src/__docs__/exports.gql-error.stories.mdx +23 -0
- package/src/__docs__/exports.gql-errors.stories.mdx +20 -0
- package/src/__docs__/exports.gql-router.stories.mdx +29 -0
- package/src/__docs__/exports.has-unfulfilled-requests.stories.mdx +20 -0
- package/src/{components/intercept-requests.md → __docs__/exports.intercept-requests.stories.mdx} +16 -1
- package/src/__docs__/exports.intialize-cache.stories.mdx +29 -0
- package/src/__docs__/exports.remove-all-from-cache.stories.mdx +24 -0
- package/src/__docs__/exports.remove-from-cache.stories.mdx +25 -0
- package/src/__docs__/exports.request-fulfillment.stories.mdx +36 -0
- package/src/__docs__/exports.scoped-in-memory-cache.stories.mdx +92 -0
- package/src/__docs__/exports.serializable-in-memory-cache.stories.mdx +112 -0
- package/src/__docs__/exports.status.stories.mdx +31 -0
- package/src/{components/track-data.md → __docs__/exports.track-data.stories.mdx} +15 -0
- package/src/__docs__/exports.use-cached-effect.stories.mdx +41 -0
- package/src/__docs__/exports.use-gql.stories.mdx +73 -0
- package/src/__docs__/exports.use-hydratable-effect.stories.mdx +43 -0
- package/src/__docs__/exports.use-server-effect.stories.mdx +50 -0
- package/src/__docs__/exports.use-shared-cache.stories.mdx +30 -0
- package/src/__docs__/exports.when-client-side.stories.mdx +33 -0
- package/src/__docs__/types.cached-response.stories.mdx +29 -0
- package/src/__docs__/types.error-options.stories.mdx +21 -0
- package/src/__docs__/types.gql-context.stories.mdx +20 -0
- package/src/__docs__/types.gql-fetch-fn.stories.mdx +24 -0
- package/src/__docs__/types.gql-fetch-options.stories.mdx +24 -0
- package/src/__docs__/types.gql-operation-type.stories.mdx +24 -0
- package/src/__docs__/types.gql-operation.stories.mdx +67 -0
- package/src/__docs__/types.response-cache.stories.mdx +33 -0
- package/src/__docs__/types.result.stories.mdx +39 -0
- package/src/__docs__/types.scoped-cache.stories.mdx +27 -0
- package/src/__docs__/types.valid-cache-data.stories.mdx +23 -0
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +0 -80
- package/src/__tests__/generated-snapshot.test.js +0 -24
- package/src/components/__tests__/data.test.js +149 -128
- package/src/components/data.js +22 -112
- package/src/components/intercept-requests.js +1 -1
- package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +8 -8
- package/src/hooks/__tests__/use-cached-effect.test.js +507 -0
- package/src/hooks/__tests__/use-gql-router-context.test.js +133 -0
- package/src/hooks/__tests__/use-gql.test.js +1 -30
- package/src/hooks/__tests__/use-hydratable-effect.test.js +705 -0
- package/src/hooks/__tests__/use-server-effect.test.js +90 -11
- package/src/hooks/use-cached-effect.js +225 -0
- package/src/hooks/use-gql-router-context.js +50 -0
- package/src/hooks/use-gql.js +22 -52
- package/src/hooks/use-hydratable-effect.js +206 -0
- package/src/hooks/use-request-interception.js +20 -23
- package/src/hooks/use-server-effect.js +42 -10
- package/src/hooks/use-shared-cache.js +13 -11
- package/src/index.js +53 -3
- package/src/util/__tests__/__snapshots__/serializable-in-memory-cache.test.js.snap +19 -0
- package/src/util/__tests__/merge-gql-context.test.js +74 -0
- package/src/util/__tests__/request-fulfillment.test.js +23 -42
- package/src/util/__tests__/request-tracking.test.js +26 -7
- package/src/util/__tests__/result-from-cache-response.test.js +19 -5
- package/src/util/__tests__/scoped-in-memory-cache.test.js +6 -85
- package/src/util/__tests__/serializable-in-memory-cache.test.js +398 -0
- package/src/util/__tests__/ssr-cache.test.js +52 -52
- package/src/util/data-error.js +58 -0
- package/src/util/get-gql-data-from-response.js +3 -2
- package/src/util/gql-error.js +19 -11
- package/src/util/merge-gql-context.js +34 -0
- package/src/util/request-fulfillment.js +49 -46
- package/src/util/request-tracking.js +69 -15
- package/src/util/result-from-cache-response.js +12 -16
- package/src/util/scoped-in-memory-cache.js +24 -47
- package/src/util/serializable-in-memory-cache.js +49 -0
- package/src/util/ssr-cache.js +9 -8
- package/src/util/status.js +30 -0
- package/src/util/types.js +18 -1
- package/docs.md +0 -122
package/legacy-docs.md
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-data",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@babel/runtime": "^7.16.3",
|
|
17
|
-
"@khanacademy/wonder-blocks-core": "^4.
|
|
17
|
+
"@khanacademy/wonder-blocks-core": "^4.3.0"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"@khanacademy/wonder-stuff-core": "^0.1.2",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Overview"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# Wonder Blocks Data
|
|
13
|
+
|
|
14
|
+
Wonder Blocks Data provides components, hooks, and additional APIs to make working with asynchronous requests easier, both client-side and server-side.
|
|
15
|
+
|
|
16
|
+
* [GraphQL](/docs/data-graphql--page)
|
|
17
|
+
* [Server-Side Rendering and Hydration](/docs/data-server-side-rendering-and-hydration--page)
|
|
18
|
+
* [Testing](/docs/data-testing--page)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / GraphQL"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# GraphQL in Wonder Blocks Data
|
|
13
|
+
|
|
14
|
+
Wonder Blocks Data provides some utility types and functionality to assist in performing GraphQL requests. To use them, your React app should include the [`GqlRouter`](/docs/data-exports-gqlrouter--page) component to specify the method responsible for actually making the GraphQL requests, as well as any default context that requests should include.
|
|
15
|
+
|
|
16
|
+
The [`GqlRouter`](/docs/data-exports-gqlrouter--page) component uses React context to convey this information to the [`useGql`](/docs/data-exports-usegql--page) hook, which provides a wrapper to the fetch function, allowing for simplified invocation of requests that can merge context changes with the default context as well as only provide context and variables when needed.
|
|
17
|
+
|
|
18
|
+
## Testing
|
|
19
|
+
|
|
20
|
+
By using the [`GqlRouter`](/docs/data-exports-gqlrouter--page) in combination with the [`mockGqlFetch()`](/docs/testing-exports-mockgqlfetch) API from Wonder Blocks Testing, you can easily mock your GraphQL responses in tests and stories.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
const mockFetch = mockGqlFetch()
|
|
24
|
+
.mockOperation(...);
|
|
25
|
+
|
|
26
|
+
<GqlRouter fetch={mockFetch} defaultContext={{}}>
|
|
27
|
+
<MyComponent />
|
|
28
|
+
</GqlRouter>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Server-side rendering and hydration of GraphQL
|
|
32
|
+
|
|
33
|
+
Server-side rendering and hydration of GraphQL data can be achieved by combining the [`useGql`](/docs/data-exports-usegql--page) hook with the [`useHydrationEffect`](/docs/data-exports/usehydrationeffect--page) hook.
|
|
34
|
+
|
|
35
|
+
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,185 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Server-side Rendering and Hydration"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# Server-side Rendering (SSR)
|
|
13
|
+
|
|
14
|
+
Wonder Blocks Data provides components, hooks, and additional APIs to make server-side rendering and hydration of asynchronous requests easier.
|
|
15
|
+
|
|
16
|
+
The [`Data`](/docs/data-exports-data--page) component and the [`useHydratableEffect`](/docs/data-exports-usehydratableeffect--page) hook (which [`Data`](/docs/data-exports-data--page) uses internally) both
|
|
17
|
+
support server-side rendering and hydration.
|
|
18
|
+
|
|
19
|
+
These APIs have been designed to make it easy for everyday development. The developer does not need to consider the server-side process nor hydration when requiring asynchronous data. Instead, they use [`Data`](/docs/data-exports-data--page) or [`useHydratableEffect`](/docs/data-exports-usehydratableeffect--page) to get the data they need.
|
|
20
|
+
|
|
21
|
+
## Integrating into server-side rendering
|
|
22
|
+
|
|
23
|
+
Generally, server-side rendering has a loop that renders the page, determines what asynchronous activity needs to be resolved, resolves and caches that activity, then renders again, ensuring that as much of the page as possible has been rendered.
|
|
24
|
+
|
|
25
|
+
In order for the requests made through [`Data`](/docs/data-exports-data--page) and [`useHydratableEffect`](/docs/data-exports-usehydratableeffect--page) use to be fulfilled on the server and subsequently hydrated on the client, the server responsible for rendering the page must be modified to track and capture the requests and their responses.
|
|
26
|
+
|
|
27
|
+
First, Wonder Blocks Data has to be told to work in server-side mode:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
31
|
+
|
|
32
|
+
Server.setServerSide();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
By setting Wonder Blocks into server mode, requests will not be fulfilled during rendering (there would be no point since server-side rendering does not mount components and as such, cannot update the rendered component tree with the result of asynchronous requests). Instead, we are now ready to track these requests.
|
|
36
|
+
|
|
37
|
+
### Tracking and fulfilling requests
|
|
38
|
+
|
|
39
|
+
To track the requests, the React node being rendered must be wrapped in the [`TrackData`](/docs/data-exports-trackdata--page) component.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
const trackedElement = (
|
|
43
|
+
<TrackData>
|
|
44
|
+
{theNodeTheClientNormallyRenders}
|
|
45
|
+
</TrackData>
|
|
46
|
+
);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The [`TrackData`](/docs/data-exports-trackdata--page) component is responsible for capturing the requests that were made during a render of the given React node. Of course to do that, we have to render the node. Rendering is usually performed by `renderToString` from the `react-dom/server` import.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import {renderToString} from "react-dom/server";
|
|
53
|
+
|
|
54
|
+
renderToString(trackedElement);
|
|
55
|
+
```
|
|
56
|
+
|
|
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
|
+
|
|
59
|
+
To determine which path to take, we can use [`hasUnfullfilledRequests`](/docs/data-exports-hasunfulfilledrequests--page), and then based off that result, either fulfill the requests, or finish rendering our page.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
if (hasUnfulfilledRequests()) {
|
|
63
|
+
await fulfillAllDataRequests();
|
|
64
|
+
|
|
65
|
+
// Render again.
|
|
66
|
+
// ...
|
|
67
|
+
} else {
|
|
68
|
+
// Finish rendering the page.
|
|
69
|
+
// ...
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
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
|
+
|
|
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 `fulfillAllDataRequests()`, regardless of whether it actually had any pending requests or not.
|
|
76
|
+
|
|
77
|
+
### Putting it all together
|
|
78
|
+
|
|
79
|
+
> 😱 Make sure you safely embed strings in your rendered pages to avoid injection bugs. This example is brief for the purposes of illustration.
|
|
80
|
+
|
|
81
|
+
A function for server-side rendering with Wonder Blocks Data could look something like this.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
85
|
+
import {
|
|
86
|
+
TrackData,
|
|
87
|
+
hasUnfulfilledRequests,
|
|
88
|
+
fulfillAllDataRequests,
|
|
89
|
+
clearSharedCache,
|
|
90
|
+
} from "@khanacademy/wonder-blocks-data";
|
|
91
|
+
|
|
92
|
+
// Don't forget to import your app!
|
|
93
|
+
import App from "./App.js";
|
|
94
|
+
|
|
95
|
+
async function renderApp(): Promise<string> {
|
|
96
|
+
// Tell Wonder Blocks we are server-side.
|
|
97
|
+
Server.setServerSide();
|
|
98
|
+
|
|
99
|
+
// Wrap the app with our tracking code.
|
|
100
|
+
const trackedElement = (
|
|
101
|
+
<TrackData>
|
|
102
|
+
<App />
|
|
103
|
+
</TrackData>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Now, loop around rendering and fulfilling until we have nothing left
|
|
107
|
+
// to fulfill.
|
|
108
|
+
let hydrationCache = null;
|
|
109
|
+
let renderedComponent = null;
|
|
110
|
+
do {
|
|
111
|
+
/*
|
|
112
|
+
* Each render has to be isolated, so we have to make sure we clear the
|
|
113
|
+
* shared cache used by the `useSharedCache` hook as this is transient
|
|
114
|
+
* cache that does not itself get directly hydrated.
|
|
115
|
+
*/
|
|
116
|
+
clearSharedCache();
|
|
117
|
+
|
|
118
|
+
// Render the tracked component.
|
|
119
|
+
renderedComponent = renderToString(trackedElement);
|
|
120
|
+
|
|
121
|
+
if (hasUnfulfilledRequests()) {
|
|
122
|
+
// Fulfill any pending requests.
|
|
123
|
+
await fulfillAllDataRequests();
|
|
124
|
+
|
|
125
|
+
// We can't leave the loop yet as we want to render the element
|
|
126
|
+
// again with the newly fulfilled data.
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
// We rendered with all the data fulfilled, so we can grab the
|
|
132
|
+
// hydration cache contents and exit the loop now.
|
|
133
|
+
hydrationCache = await fulfillAllDataRequests();
|
|
134
|
+
} while (hydrationCache == null);
|
|
135
|
+
|
|
136
|
+
// Finally, render the page with our hydration cache and rendered
|
|
137
|
+
// component.
|
|
138
|
+
return `
|
|
139
|
+
<html>
|
|
140
|
+
<head>
|
|
141
|
+
<title>My Page</title>
|
|
142
|
+
<script>
|
|
143
|
+
window.__WONDER_BLOCKS_DATA__ = ${safeStringify(hydrationCache)}
|
|
144
|
+
</script>
|
|
145
|
+
<script src="hydrate.js"></script>
|
|
146
|
+
</head>
|
|
147
|
+
<body>
|
|
148
|
+
<div id="my-react-app-root">
|
|
149
|
+
${renderedComponent}
|
|
150
|
+
</div>
|
|
151
|
+
</body>
|
|
152
|
+
</html>
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The above page is simplified for clarity - you would need to make sure all your scripts, fonts, etc. get loaded into the page. However, assuming all those things get loaded, the last piece need is signified above as the script tag importing `hydrate.js` - this is the script that will hydrate the client-side rendered component.
|
|
158
|
+
|
|
159
|
+
## Hydrating on the client-side
|
|
160
|
+
|
|
161
|
+
Now that you have a rendered page, the client needs to be written to properly hydrate to the exact same state as the server rendered. To do this, the hydration cache that was serialized into the page as `window.__WONDER_BLOCKS_DATA__` must be injected into the Wonder Blocks Data cache before the React `hydrate` call is made.
|
|
162
|
+
|
|
163
|
+
In the previous section, this was displayed as the `hydrate.js` script. Here is what that script might look like.
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
import {hydrate} from "react-dom";
|
|
167
|
+
import {initializeCache} from "@khanacademy/wonder-blocks-data";
|
|
168
|
+
|
|
169
|
+
// Don't forget to import your app!
|
|
170
|
+
import App from "./App.js";
|
|
171
|
+
|
|
172
|
+
initializeCache(window._WONDER_BLOCKS_DATA_);
|
|
173
|
+
|
|
174
|
+
React.hydrate(
|
|
175
|
+
// This should match whatever was passed to the server-side rendering
|
|
176
|
+
// function, renderApp, in the previous section.
|
|
177
|
+
<App />,
|
|
178
|
+
|
|
179
|
+
// The ID of the element to host the hydrated component has to match what
|
|
180
|
+
// the server-rendered page uses.
|
|
181
|
+
document.getElementById("my-react-app-root"),
|
|
182
|
+
);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
That's it! The client-side hydration is complete. Underneath the hood, the hydration cache is used to hydrate the responses that the server used to render the page. And if you render a page that was not server-side rendered, the [`Data`](/docs/data-exports-data--page) and [`useHydratableEffect`](/docs/data-exports-usehydratableeffect--page) know exactly what to do and will make the requests client-side instead.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Testing"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# Testing Support
|
|
13
|
+
|
|
14
|
+
Wonder Blocks Data has been designed to support testing in a variety of environments including jest and storybook. In order to support the various ways in which folks need to test their code, we have considered a number of approaches to testing Wonder Blocks Data.
|
|
15
|
+
|
|
16
|
+
## Spies
|
|
17
|
+
If you are writing unit tests, you may just want to spy on the methods you are calling using `jest.spyOn` or similar. This can be a really easy way to intercept the request handler passed to the [`useCachedEffect`](/docs/data-exports-usecachedeffect--page) hook, for example, and check that it is the handler you expect.
|
|
18
|
+
|
|
19
|
+
## Interceptors
|
|
20
|
+
|
|
21
|
+
Each request used by Wonder Blocks Data has to have an identifier. The [`InterceptRequests`](/docs/data-exports-interceptrequests--page) component allows you to wrap the code under test with an interceptor. Interceptors are given the request identifier and get to choose, based off that identifier, if they want to provide their own response rather than let the original request handler deal with it.
|
|
22
|
+
|
|
23
|
+
Multiple interceptors can be registered by nesting the [`InterceptRequests`](/docs/data-exports-interceptrequests--page) component as is necessary. Registered interceptors are invoked in ancestral order, with the nearest ancestor to the intercepted request being invoked first.
|
|
24
|
+
|
|
25
|
+
When hooks like [`useServerEffect`](/docs/data-exports-useservereffect--page), [`useCachedEffect`](/docs/data-exports-usecachedeffect--page), or [`useHydratedEffect`](/docs/data-exports-usehydratedeffect--page) run, they get the chain of registered interceptors and chain those with the original handler in order to determine what to actually do when executing the request.
|
|
26
|
+
|
|
27
|
+
This allows you to mock out requests in unit tests, stories, and other scenarios.
|
|
28
|
+
|
|
29
|
+
## GqlRouter, mockGqlFetch, and RespondWith
|
|
30
|
+
|
|
31
|
+
If you are testing GraphQL operations, you can configure [`GqlRouter`](/docs/data-exports-gqlrouter--page) with your own function for the `fetch` prop. However, crafting the right response to give
|
|
32
|
+
the result you want is a bit tricky.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
const myFakeGqlFetch = (
|
|
36
|
+
operation: GqlOperation<TData, TVariables>,
|
|
37
|
+
variables: ?TVariables,
|
|
38
|
+
context: TContext,
|
|
39
|
+
): Promise<Response> {
|
|
40
|
+
if (operation.id === "myQuery" && variables?.someVar === 5) {
|
|
41
|
+
return Promise.resolve({
|
|
42
|
+
status: 200,
|
|
43
|
+
text: () =>
|
|
44
|
+
Promise.resolve(
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
data: {
|
|
47
|
+
myQuery: {
|
|
48
|
+
someField: "someValue",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Promise.resolve({
|
|
57
|
+
status: 404,
|
|
58
|
+
text: () => Promise.resolve(JSON.stringify({})),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
<GqlRouter fetch={myFakeGqlFetch} defaultContext={{some: "sort of context"}}>
|
|
63
|
+
<ComponentUnderTest />
|
|
64
|
+
</GqlRouter>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
As shown above, you can interrogate parts of the requested operation to decide how to respond. However, this can get cumbersome if you have GraphQL requests nested in more complex components as each test has to mock out suitable responses for each one. To help with this the Wonder Blocks Testing package provides a [`RespondWith`](/docs/testing-exports-respondwith--page) type for defining responses that fit a specific scenario, and the [`mockGqlFetch()`](/docs/testing-exports-mockgqlfetch--page) API.
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
const myFakeGqlFetch = mockGqlFetch()
|
|
71
|
+
.mockOperationOnce(
|
|
72
|
+
{
|
|
73
|
+
operation: MyQueryOperation,
|
|
74
|
+
variables: {
|
|
75
|
+
someVar: 5,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
RespondWith.success({
|
|
79
|
+
data: {
|
|
80
|
+
myQuery: {
|
|
81
|
+
someField: "someValue",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
<GqlRouter fetch={myFakeGqlFetch} defaultContext={{some: "sort of context"}}>
|
|
88
|
+
<ComponentUnderTest />
|
|
89
|
+
</GqlRouter>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
In the above example, we now only mock our specific operation once. If something
|
|
93
|
+
tries to request this data a second time, it will give an error instead. Not only that, but with a little refactoring, we can create a helper to set this mock up that others can call if they need to mock our operation for their own tests.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const mockMyQuery = (mockGqlFetchFn: GqlFetchMockFn): GqlFetchMockFn =>
|
|
97
|
+
mockGqlFetchFn()
|
|
98
|
+
.mockOperationOnce(
|
|
99
|
+
{
|
|
100
|
+
operation: MyQueryOperation,
|
|
101
|
+
variables: {
|
|
102
|
+
someVar: 5,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
RespondWith.success({
|
|
106
|
+
data: {
|
|
107
|
+
myQuery: {
|
|
108
|
+
someField: "someValue",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const myFakeGqlFetch = mockMyQuery(mockGqlFetch());
|
|
115
|
+
|
|
116
|
+
<GqlRouter fetch={myFakeGqlFetch} defaultContext={{some: "sort of context"}}>
|
|
117
|
+
<ComponentUnderTest />
|
|
118
|
+
</GqlRouter>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Now, using a compose function, multiple mocks can be setup on the same `mockGqlFetch` instance.
|
|
122
|
+
|
|
123
|
+
For more details on this and other testing utilities, see the [Wonder Blocks Testing documentation](/docs/testing-overview--page).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Exports / clearSharedCache()"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# clearSharedCache()
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
clearSharedCache(scope?: string): void;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The `clearSharedCache` method can be used to clear the shared in-memory cache used by the [`useSharedCache`](/docs/data-exports-clearsharedcache--page) hook. Either a single scope or all scopes can be cleared.
|
|
19
|
+
|
|
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.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Exports / DataError"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# DataError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
new DataError(
|
|
17
|
+
message: string,
|
|
18
|
+
kind: $Values<typeof DataErrors>,
|
|
19
|
+
options?: ErrorOptions,
|
|
20
|
+
);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The `DataError` class is a derivation of the Wonder Blocks Core `KindError` (which is itself a derivation of `Error`). It is used by the Wonder Blocks Data framework to encapsulate errors that can occur when using its API. The different kinds of errors supported are defined by the [`DataErrors`](/docs/data-exports-dataerrors--page) export.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Exports / DataErrors"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# DataErrors
|
|
13
|
+
|
|
14
|
+
This export defines the error taxonomy used by Wonder Blocks Data to describe the errors that can occur. These are for use with [`DataError`](/docs/data-exports-dataerror--page).
|
|
15
|
+
|
|
16
|
+
| Kind | Description |
|
|
17
|
+
| ---- | ----------- |
|
|
18
|
+
| `DataErrors.Unknown` | The kind of error is not known. |
|
|
19
|
+
| `DataErrors.Internal` | The error is internal to the executing code. |
|
|
20
|
+
| `DataErrors.InvalidInput` | There was a problem with the provided input. |
|
|
21
|
+
| `DataErrors.Network` | A network error occurred. |
|
|
22
|
+
| `DataErrors.Parse` | Response could not be parsed. |
|
|
23
|
+
| `DataErrors.Hydrated` | An error that occurred during SSR and was hydrated from cache |
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
import {Data} from "../index.js";
|
|
3
|
+
|
|
4
|
+
<Meta
|
|
5
|
+
title="Data / Exports / Data"
|
|
6
|
+
component={Data}
|
|
7
|
+
parameters={{
|
|
8
|
+
chromatic: {
|
|
9
|
+
disableSnapshot: true,
|
|
10
|
+
},
|
|
11
|
+
}}
|
|
12
|
+
/>
|
|
13
|
+
|
|
14
|
+
# Data
|
|
15
|
+
|
|
1
16
|
The `Data` component is the frontend piece of our data architecture.
|
|
2
17
|
It describes a data requirement in terms of a handler and an identifier.
|
|
3
18
|
It also has props to govern hydrate behavior as well as loading and client-side
|
|
@@ -110,9 +125,6 @@ initializeCache({
|
|
|
110
125
|
DATA: {
|
|
111
126
|
data: "I'm DATA from the hydration cache"
|
|
112
127
|
},
|
|
113
|
-
ERROR: {
|
|
114
|
-
error: "I'm an ERROR from hydration cache"
|
|
115
|
-
}
|
|
116
128
|
});
|
|
117
129
|
|
|
118
130
|
<View>
|
|
@@ -130,20 +142,5 @@ initializeCache({
|
|
|
130
142
|
}}
|
|
131
143
|
</Data>
|
|
132
144
|
</View>
|
|
133
|
-
<Strut size={Spacing.small_12} />
|
|
134
|
-
<View>
|
|
135
|
-
<Body>This cache has error!</Body>
|
|
136
|
-
<Data handler={myHandler} requestId="ERROR">
|
|
137
|
-
{(result) => {
|
|
138
|
-
if (result.status !== "error") {
|
|
139
|
-
return "If you see this, the example is broken!";
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return (
|
|
143
|
-
<BodyMonospace style={{color: Color.red}}>ERROR: {result.error}</BodyMonospace>
|
|
144
|
-
);
|
|
145
|
-
}}
|
|
146
|
-
</Data>
|
|
147
|
-
</View>
|
|
148
145
|
</View>
|
|
149
146
|
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Exports / fulfillAllDataRequests()"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# fulfillAllDataRequests()
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
fulfillAllDataRequests(): Promise<ResponseCache>;
|
|
16
|
+
```
|
|
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 `fulfillAllDataRequests`.
|
|
19
|
+
|
|
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
|
+
|
|
22
|
+
The returned copy of the data cache can be used with the [`initializeCache`](/docs/data-exports-initializecache--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
|
+
|
|
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,23 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Exports / GqlError"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# GqlError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
new GqlError(
|
|
17
|
+
message: string,
|
|
18
|
+
kind: $Values<typeof GqlErrors>,
|
|
19
|
+
options?: ErrorOptions,
|
|
20
|
+
);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The `GqlError` class is a derivation of the Wonder Blocks Core `KindError` (which is itself a derivation of `Error`). It is used by the Wonder Blocks Data GraphQL framework to encapsulate errors that can occur within the GraphQL API. The different kinds of errors supported are defined by the [`GqlErrors`](/docs/data-exports-gqlerrors--page) export.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Exports / GqlErrors"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# GqlErrors
|
|
13
|
+
|
|
14
|
+
This export defines the error taxonomy used by Wonder Blocks Data to describe the GraphQL-related errors that can occur with GraphQL requests. These are for use with [`GqlError`](/docs/data-exports-gqlerror--page).
|
|
15
|
+
|
|
16
|
+
| Kind | Description |
|
|
17
|
+
| ---- | ----------- |
|
|
18
|
+
| `GqlErrors.Unknown` | The type of error is unknown. |
|
|
19
|
+
| `GqlErrors.BadResponse` | Response does not have the correct structure for a GraphQL response. |
|
|
20
|
+
| `GqlErrors.ErrorResult` | A valid GraphQL result with errors field in the payload. |
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
import {GqlRouter} from "../index.js";
|
|
3
|
+
|
|
4
|
+
<Meta
|
|
5
|
+
title="Data / Exports / GqlRouter"
|
|
6
|
+
component={GqlRouter}
|
|
7
|
+
parameters={{
|
|
8
|
+
chromatic: {
|
|
9
|
+
disableSnapshot: true,
|
|
10
|
+
},
|
|
11
|
+
}}
|
|
12
|
+
/>
|
|
13
|
+
|
|
14
|
+
# GqlRouter
|
|
15
|
+
|
|
16
|
+
The `GqlRouter` component is used to define the default context and fetch function for performing GraphQL requests using the Wonder Blocks Data API.
|
|
17
|
+
|
|
18
|
+
The [`useGql`](/docs/data-exports-usegql--page) (and therefore any hooks or components that use it) requires at least one `GqlRouter` in the component hierarchy above it or it will throw an error.
|
|
19
|
+
|
|
20
|
+
The `defaultContext` is specific to the needs of your specific implementation, as is the `fetch` function. Currently, a default for the fetch function is not provided by Wonder Blocks Data and should be written to the specification of the server that will fulfill your GraphQL responses. The `fetch` function must conform to the [`GqlFetchFn<>`](/docs/data-types-gqlfetchfn--page) type.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<GqlRouter defaultContext={{a: "1", b: "2"}} fetch={gqlFetchFn}>
|
|
24
|
+
<div>
|
|
25
|
+
The code that will ultimately use Wonder Blocks Data-based GraphQL
|
|
26
|
+
operations goes here.
|
|
27
|
+
</div>
|
|
28
|
+
</GqlRouter>
|
|
29
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
<Meta
|
|
4
|
+
title="Data / Exports / hasUnfulfilledRequests()"
|
|
5
|
+
parameters={{
|
|
6
|
+
chromatic: {
|
|
7
|
+
disableSnapshot: true,
|
|
8
|
+
},
|
|
9
|
+
}}
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
# hasUnfulfilledRequests()
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
hasUnfulfilledRequests(): boolean;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
When performing server-side rendering (SSR), any requests that have been tracked will cause this method to return `true`. Once [`fulfillAllDataRequests`](/docs/data-exports-fulfillalldatarequests--page) has been called and the promise is settled, this method will return `false` to indicate that there are no more pending requests.
|
|
19
|
+
|
|
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).
|
package/src/{components/intercept-requests.md → __docs__/exports.intercept-requests.stories.mdx}
RENAMED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
import {Meta} from "@storybook/addon-docs";
|
|
2
|
+
import {InterceptRequests} from "../index.js";
|
|
3
|
+
|
|
4
|
+
<Meta
|
|
5
|
+
title="Data / Exports / InterceptRequests"
|
|
6
|
+
component={InterceptRequests}
|
|
7
|
+
parameters={{
|
|
8
|
+
chromatic: {
|
|
9
|
+
disableSnapshot: true,
|
|
10
|
+
},
|
|
11
|
+
}}
|
|
12
|
+
/>
|
|
13
|
+
|
|
14
|
+
# InterceptRequests
|
|
15
|
+
|
|
1
16
|
When you want to generate tests that check the loading state and
|
|
2
17
|
subsequent loaded state are working correctly for your uses of `Data` you can
|
|
3
18
|
use the `InterceptRequests` component. You can also use this component to
|
|
@@ -15,7 +30,7 @@ different request IDs..
|
|
|
15
30
|
The `interceptor` intercept function has the form:
|
|
16
31
|
|
|
17
32
|
```js static
|
|
18
|
-
(requestId: string) => ?Promise
|
|
33
|
+
(requestId: string) => ?Promise<TData>;
|
|
19
34
|
```
|
|
20
35
|
|
|
21
36
|
If this method returns `null`, then the next interceptor in the chain is
|