@khanacademy/wonder-blocks-data 3.1.3 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/es/index.js +408 -349
  3. package/dist/index.js +599 -494
  4. package/docs.md +17 -35
  5. package/package.json +1 -1
  6. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +7 -46
  7. package/src/__tests__/generated-snapshot.test.js +60 -126
  8. package/src/components/__tests__/data.test.js +373 -313
  9. package/src/components/__tests__/intercept-requests.test.js +58 -0
  10. package/src/components/data.js +139 -21
  11. package/src/components/data.md +38 -69
  12. package/src/components/intercept-context.js +6 -3
  13. package/src/components/intercept-requests.js +69 -0
  14. package/src/components/intercept-requests.md +54 -0
  15. package/src/components/track-data.md +9 -23
  16. package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +17 -0
  17. package/src/hooks/__tests__/use-gql.test.js +1 -0
  18. package/src/hooks/__tests__/use-request-interception.test.js +255 -0
  19. package/src/hooks/__tests__/use-server-effect.test.js +217 -0
  20. package/src/hooks/__tests__/use-shared-cache.test.js +307 -0
  21. package/src/hooks/use-gql.js +36 -23
  22. package/src/hooks/use-request-interception.js +54 -0
  23. package/src/hooks/use-server-effect.js +45 -0
  24. package/src/hooks/use-shared-cache.js +106 -0
  25. package/src/index.js +18 -20
  26. package/src/util/__tests__/__snapshots__/scoped-in-memory-cache.test.js.snap +19 -0
  27. package/src/util/__tests__/request-fulfillment.test.js +42 -85
  28. package/src/util/__tests__/request-tracking.test.js +72 -191
  29. package/src/util/__tests__/{result-from-cache-entry.test.js → result-from-cache-response.test.js} +9 -10
  30. package/src/util/__tests__/scoped-in-memory-cache.test.js +396 -0
  31. package/src/util/__tests__/ssr-cache.test.js +639 -0
  32. package/src/util/request-fulfillment.js +36 -44
  33. package/src/util/request-tracking.js +62 -75
  34. package/src/util/{result-from-cache-entry.js → result-from-cache-response.js} +10 -13
  35. package/src/util/scoped-in-memory-cache.js +149 -0
  36. package/src/util/ssr-cache.js +206 -0
  37. package/src/util/types.js +43 -108
  38. package/src/components/__tests__/intercept-data.test.js +0 -87
  39. package/src/components/intercept-data.js +0 -77
  40. package/src/components/intercept-data.md +0 -65
  41. package/src/hooks/__tests__/use-data.test.js +0 -826
  42. package/src/hooks/use-data.js +0 -143
  43. package/src/util/__tests__/memory-cache.test.js +0 -446
  44. package/src/util/__tests__/request-handler.test.js +0 -121
  45. package/src/util/__tests__/response-cache.test.js +0 -879
  46. package/src/util/memory-cache.js +0 -187
  47. package/src/util/request-handler.js +0 -42
  48. package/src/util/request-handler.md +0 -51
  49. package/src/util/response-cache.js +0 -213
@@ -0,0 +1,58 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import {render} from "@testing-library/react";
4
+
5
+ import InterceptContext from "../intercept-context.js";
6
+ import InterceptRequests from "../intercept-requests.js";
7
+
8
+ describe("InterceptRequests", () => {
9
+ afterEach(() => {
10
+ jest.resetAllMocks();
11
+ });
12
+
13
+ it("should update context with fulfillRequest method", () => {
14
+ // Arrange
15
+ const fakeHandler = (requestId): Promise<string> =>
16
+ Promise.resolve("data");
17
+ const props = {
18
+ interceptor: fakeHandler,
19
+ };
20
+ const captureContextFn = jest.fn();
21
+
22
+ // Act
23
+ render(
24
+ <InterceptRequests {...props}>
25
+ <InterceptContext.Consumer>
26
+ {captureContextFn}
27
+ </InterceptContext.Consumer>
28
+ </InterceptRequests>,
29
+ );
30
+
31
+ // Assert
32
+ expect(captureContextFn).toHaveBeenCalledWith([fakeHandler]);
33
+ });
34
+
35
+ it("should override parent InterceptRequests", () => {
36
+ // Arrange
37
+ const fakeHandler1 = jest.fn();
38
+ const fakeHandler2 = jest.fn();
39
+ const captureContextFn = jest.fn();
40
+
41
+ // Act
42
+ render(
43
+ <InterceptRequests interceptor={fakeHandler1}>
44
+ <InterceptRequests interceptor={fakeHandler2}>
45
+ <InterceptContext.Consumer>
46
+ {captureContextFn}
47
+ </InterceptContext.Consumer>
48
+ </InterceptRequests>
49
+ </InterceptRequests>,
50
+ );
51
+
52
+ // Assert
53
+ expect(captureContextFn).toHaveBeenCalledWith([
54
+ fakeHandler1,
55
+ fakeHandler2,
56
+ ]);
57
+ });
58
+ });
@@ -1,36 +1,63 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
3
 
4
- import {useData} from "../hooks/use-data.js";
4
+ import {Server} from "@khanacademy/wonder-blocks-core";
5
+ import {RequestFulfillment} from "../util/request-fulfillment.js";
6
+ import {useServerEffect} from "../hooks/use-server-effect.js";
7
+ import {useRequestInterception} from "../hooks/use-request-interception.js";
8
+ import {resultFromCachedResponse} from "../util/result-from-cache-response.js";
5
9
 
6
- import type {Result, IRequestHandler, ValidData} from "../util/types.js";
10
+ import type {Result, ValidCacheData} from "../util/types.js";
7
11
 
8
12
  type Props<
9
- /**
10
- * The type of options that the handler requires to define a request.
11
- */
12
- TOptions,
13
13
  /**
14
14
  * The type of data resolved by the handler's fulfillRequest method.
15
15
  */
16
- TData,
16
+ TData: ValidCacheData,
17
17
  > = {|
18
18
  /**
19
- * An `IRequestHandler` instance of the type this component will use to
20
- * resolve its requests.
19
+ * A unique identifier for the request.
20
+ *
21
+ * This should not be shared by other uses of this component.
22
+ */
23
+ requestId: string,
24
+
25
+ /**
26
+ * This defines how the request is fulfilled.
21
27
  *
22
- * The framework deduplicates handlers based on their `type` property.
23
- * Handlers with the same `type` property are assumed to be the same.
28
+ * If this is changed without changing the ID, there are cases where the
29
+ * old handler result may be given. This is not a supported mode of
30
+ * operation.
24
31
  */
25
- handler: IRequestHandler<TOptions, TData>,
32
+ handler: () => Promise<?TData>,
26
33
 
27
34
  /**
28
- * The handler-specific options that define what requestt is to be made.
35
+ * When true, the result will be hydrated when client-side. Otherwise,
36
+ * the request will be fulfilled for us in SSR but will be ignored during
37
+ * hydration. Only set this to false if you know some other mechanism
38
+ * will be performing hydration (such as if requests are fulfilled by
39
+ * Apollo Client but you consolidated all SSR requests using WB Data).
29
40
  *
30
- * Changing these options will only cause the data to update if the key
31
- * from `handler.getKey(options)` changes.
41
+ * Defaults to true.
32
42
  */
33
- options: TOptions,
43
+ hydrate?: boolean,
44
+
45
+ /**
46
+ * When true, the children will be rendered with the existing result
47
+ * until the pending load is completed. Otherwise, the children will be
48
+ * given a loading state until the request is fulfilled.
49
+ *
50
+ * Defaults to false.
51
+ */
52
+ showOldDataWhileLoading?: boolean,
53
+
54
+ /**
55
+ * When true, the handler will always be invoked after hydration.
56
+ * This defaults to false.
57
+ * NOTE: The request is invoked after hydration if the hydrated result
58
+ * is an error.
59
+ */
60
+ alwaysRequestOnHydration?: boolean,
34
61
 
35
62
  /**
36
63
  * A function that will render the content of this component using the
@@ -45,10 +72,101 @@ type Props<
45
72
  * requirements can be placed in a React application in a manner that will
46
73
  * support server-side rendering and efficient caching.
47
74
  */
48
- const Data = <TOptions, TData: ValidData>(
49
- props: Props<TOptions, TData>,
50
- ): React.Node => {
51
- const data = useData(props.handler, props.options);
52
- return props.children(data);
75
+ const Data = <TData: ValidCacheData>({
76
+ requestId,
77
+ handler,
78
+ children,
79
+ hydrate,
80
+ showOldDataWhileLoading,
81
+ alwaysRequestOnHydration,
82
+ }: Props<TData>): React.Node => {
83
+ const interceptedHandler = useRequestInterception(requestId, handler);
84
+
85
+ const hydrateResult = useServerEffect(
86
+ requestId,
87
+ interceptedHandler,
88
+ hydrate,
89
+ );
90
+ const [currentResult, setResult] = React.useState(hydrateResult);
91
+
92
+ // Here we make sure the request still occurs client-side as needed.
93
+ // This is for legacy usage that expects this. Eventually we will want
94
+ // to deprecate.
95
+ React.useEffect(() => {
96
+ // This is here until I can do a better documentation example for
97
+ // the TrackData docs.
98
+ // istanbul ignore next
99
+ if (Server.isServerSide()) {
100
+ return;
101
+ }
102
+
103
+ // We don't bother with this if we have hydration data and we're not
104
+ // forcing a request on hydration.
105
+ // We don't care if these things change after the first render,
106
+ // so we don't want them in the inputs array.
107
+ if (!alwaysRequestOnHydration && hydrateResult?.data != null) {
108
+ return;
109
+ }
110
+
111
+ // If we're not hydrating a result and we're not going to render
112
+ // with old data until we're loaded, we want to make sure we set our
113
+ // result to null so that we're in the loading state.
114
+ if (!showOldDataWhileLoading) {
115
+ // Mark ourselves as loading.
116
+ setResult(null);
117
+ }
118
+
119
+ // We aren't server-side, so let's make the request.
120
+ // We don't need to use our built-in request fulfillment here if we
121
+ // don't want, but it does mean we'll share inflight requests for the
122
+ // same ID and the result will be in the same format as the
123
+ // hydrated value.
124
+ let cancel = false;
125
+ RequestFulfillment.Default.fulfill(requestId, {
126
+ handler: interceptedHandler,
127
+ })
128
+ .then((result) => {
129
+ if (cancel) {
130
+ return;
131
+ }
132
+ setResult(result);
133
+ return;
134
+ })
135
+ .catch((e) => {
136
+ if (cancel) {
137
+ return;
138
+ }
139
+ /**
140
+ * We should never get here as errors in fulfillment are part
141
+ * of the `then`, but if we do.
142
+ */
143
+ // eslint-disable-next-line no-console
144
+ console.error(
145
+ `Unexpected error occurred during data fulfillment: ${e}`,
146
+ );
147
+ setResult({
148
+ error: typeof e === "string" ? e : e.message,
149
+ });
150
+ return;
151
+ });
152
+
153
+ return () => {
154
+ cancel = true;
155
+ };
156
+ // If the handler changes, we don't care. The ID is what indicates
157
+ // the request that should be made and folks shouldn't be changing the
158
+ // handler without changing the ID as well.
159
+ // In addition, we don't want to include hydrateResult nor
160
+ // alwaysRequestOnHydration as them changinng after the first pass
161
+ // is irrelevant.
162
+ // Finally, we don't want to include showOldDataWhileLoading as that
163
+ // changing on its own is also not relevant. It only matters if the
164
+ // request itself changes. All of which is to say that we only
165
+ // run this effect for the ID changing.
166
+ // eslint-disable-next-line react-hooks/exhaustive-deps
167
+ }, [requestId]);
168
+
169
+ return children(resultFromCachedResponse(currentResult));
53
170
  };
171
+
54
172
  export default Data;
@@ -1,7 +1,7 @@
1
- The `Data` component is the frontend piece of our data architecture that
2
- most folks will use. It describes a data requirement in terms of a handler, and
3
- some options. Handlers must implement the
4
- `IRequestHandler` interface.
1
+ The `Data` component is the frontend piece of our data architecture.
2
+ It describes a data requirement in terms of a handler and an identifier.
3
+ It also has props to govern hydrate behavior as well as loading and client-side
4
+ request behavior.
5
5
 
6
6
  The handler is responsible for fulfilling the request when asked to do so.
7
7
 
@@ -40,49 +40,30 @@ data or an error, we re-render.
40
40
  ```jsx
41
41
  import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
42
42
  import {View} from "@khanacademy/wonder-blocks-core";
43
- import {Data, RequestHandler} from "@khanacademy/wonder-blocks-data";
43
+ import {Data} from "@khanacademy/wonder-blocks-data";
44
44
  import {Strut} from "@khanacademy/wonder-blocks-layout";
45
45
  import Color from "@khanacademy/wonder-blocks-color";
46
46
  import Spacing from "@khanacademy/wonder-blocks-spacing";
47
47
 
48
- class MyValidHandler extends RequestHandler {
49
- constructor() {
50
- super("CACHE_MISS_HANDLER_VALID");
51
- }
52
-
53
- fulfillRequest(options) {
54
- return new Promise((resolve, reject) =>
55
- setTimeout(() => resolve("I'm DATA from a request"), 3000),
56
- );
57
- }
58
- }
59
-
60
- class MyInvalidHandler extends RequestHandler {
61
- constructor() {
62
- super("CACHE_MISS_HANDLER_ERROR");
63
- }
48
+ const myValidHandler = () => new Promise((resolve, reject) =>
49
+ setTimeout(() => resolve("I'm DATA from a request"), 3000),
50
+ );
64
51
 
65
- fulfillRequest(options) {
66
- return new Promise((resolve, reject) =>
67
- setTimeout(() => reject("I'm an ERROR from a request"), 3000),
68
- );
69
- }
70
- }
71
-
72
- const valid = new MyValidHandler();
73
- const invalid = new MyInvalidHandler();
52
+ const myInvalidHandler = () => new Promise((resolve, reject) =>
53
+ setTimeout(() => reject("I'm an ERROR from a request"), 3000),
54
+ );
74
55
 
75
56
  <View>
76
57
  <View>
77
58
  <Body>This request will succeed and give us data!</Body>
78
- <Data handler={valid} options={{some: "options"}}>
79
- {({loading, data}) => {
80
- if (loading) {
59
+ <Data handler={myValidHandler} requestId="VALID">
60
+ {(result) => {
61
+ if (result.status === "loading") {
81
62
  return "Loading...";
82
63
  }
83
64
 
84
65
  return (
85
- <BodyMonospace>{data}</BodyMonospace>
66
+ <BodyMonospace>{result.data}</BodyMonospace>
86
67
  );
87
68
  }}
88
69
  </Data>
@@ -90,14 +71,14 @@ const invalid = new MyInvalidHandler();
90
71
  <Strut size={Spacing.small_12} />
91
72
  <View>
92
73
  <Body>This request will go boom and give us an error!</Body>
93
- <Data handler={invalid} options={{some: "options"}}>
94
- {({loading, error}) => {
95
- if (loading) {
74
+ <Data handler={myInvalidHandler} requestId="INVALID">
75
+ {(result) => {
76
+ if (result.status === "loading") {
96
77
  return "Loading...";
97
78
  }
98
79
 
99
80
  return (
100
- <BodyMonospace style={{color: Color.red}}>ERROR: {error}</BodyMonospace>
81
+ <BodyMonospace style={{color: Color.red}}>ERROR: {result.error}</BodyMonospace>
101
82
  );
102
83
  }}
103
84
  </Data>
@@ -114,49 +95,37 @@ populated using the `initializeCache` method before rendering.
114
95
  ```jsx
115
96
  import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
116
97
  import {View} from "@khanacademy/wonder-blocks-core";
117
- import {Data, RequestHandler, initializeCache} from "@khanacademy/wonder-blocks-data";
98
+ import {Data, initializeCache} from "@khanacademy/wonder-blocks-data";
118
99
  import {Strut} from "@khanacademy/wonder-blocks-layout";
119
100
  import Color from "@khanacademy/wonder-blocks-color";
120
101
  import Spacing from "@khanacademy/wonder-blocks-spacing";
121
102
 
122
- class MyHandler extends RequestHandler {
123
- constructor() {
124
- super("CACHE_HIT_HANDLER");
125
- }
126
-
127
- /**
128
- * fulfillRequest should not get called as we already have data cached.
129
- */
130
- fulfillRequest(options) {
131
- throw new Error(
132
- "If you're seeing this error, the examples are broken and data isn't in the cache that should be.",
133
- );
134
- }
135
- }
103
+ const myHandler = () => {
104
+ throw new Error(
105
+ "If you're seeing this error, the examples are broken and data isn't in the cache that should be.",
106
+ );
107
+ };
136
108
 
137
- const handler = new MyHandler();
138
109
  initializeCache({
139
- CACHE_HIT_HANDLER: {
140
- DATA: {
141
- data: "I'm DATA from the hydration cache"
142
- },
143
- ERROR: {
144
- error: "I'm an ERROR from hydration cache"
145
- }
110
+ DATA: {
111
+ data: "I'm DATA from the hydration cache"
112
+ },
113
+ ERROR: {
114
+ error: "I'm an ERROR from hydration cache"
146
115
  }
147
116
  });
148
117
 
149
118
  <View>
150
119
  <View>
151
120
  <Body>This cache has data!</Body>
152
- <Data handler={handler} options={"DATA"}>
153
- {({loading, data}) => {
154
- if (loading) {
121
+ <Data handler={myHandler} requestId="DATA">
122
+ {(result) => {
123
+ if (result.status !== "success") {
155
124
  return "If you see this, the example is broken!";
156
125
  }
157
126
 
158
127
  return (
159
- <BodyMonospace>{data}</BodyMonospace>
128
+ <BodyMonospace>{result.data}</BodyMonospace>
160
129
  );
161
130
  }}
162
131
  </Data>
@@ -164,14 +133,14 @@ initializeCache({
164
133
  <Strut size={Spacing.small_12} />
165
134
  <View>
166
135
  <Body>This cache has error!</Body>
167
- <Data handler={handler} options={"ERROR"}>
168
- {({loading, error}) => {
169
- if (loading) {
136
+ <Data handler={myHandler} requestId="ERROR">
137
+ {(result) => {
138
+ if (result.status !== "error") {
170
139
  return "If you see this, the example is broken!";
171
140
  }
172
141
 
173
142
  return (
174
- <BodyMonospace style={{color: Color.red}}>ERROR: {error}</BodyMonospace>
143
+ <BodyMonospace style={{color: Color.red}}>ERROR: {result.error}</BodyMonospace>
175
144
  );
176
145
  }}
177
146
  </Data>
@@ -1,14 +1,17 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
+ import type {ValidCacheData} from "../util/types.js";
3
4
 
4
- import type {InterceptContextData} from "../util/types.js";
5
+ type InterceptContextData = $ReadOnlyArray<
6
+ (requestId: string) => ?Promise<?ValidCacheData>,
7
+ >;
5
8
 
6
9
  /**
7
- * InterceptContext defines a map from handler type to interception methods.
10
+ * InterceptContext defines a map from request ID to interception methods.
8
11
  *
9
12
  * INTERNAL USE ONLY
10
13
  */
11
14
  const InterceptContext: React.Context<InterceptContextData> =
12
- React.createContext<InterceptContextData>({});
15
+ React.createContext<InterceptContextData>([]);
13
16
 
14
17
  export default InterceptContext;
@@ -0,0 +1,69 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ import InterceptContext from "./intercept-context.js";
5
+
6
+ import type {ValidCacheData} from "../util/types.js";
7
+
8
+ type Props<TData: ValidCacheData> = {|
9
+ /**
10
+ * Called to intercept and possibly handle the request.
11
+ * If this returns null, the request will be handled by ancestor
12
+ * any ancestor interceptors, and ultimately, the original request
13
+ * handler, otherwise, this interceptor is handling the request.
14
+ *
15
+ * Interceptors are called in ancestor precedence, with the closest
16
+ * interceptor ancestor being called first, and the furthest ancestor
17
+ * being called last.
18
+ *
19
+ * Beware: Interceptors do not care about what data they are intercepting,
20
+ * so make sure to only intercept requests that you recognize from the
21
+ * identifier.
22
+ */
23
+ interceptor: (requestId: string) => ?Promise<?TData>,
24
+
25
+ /**
26
+ * The children to render within this component. Any requests by `Data`
27
+ * components that use same ID as this component will be intercepted.
28
+ * If `InterceptRequests` is used within `children`, that interception will
29
+ * be given a chance to intercept first.
30
+ */
31
+ children: React.Node,
32
+ |};
33
+
34
+ /**
35
+ * This component provides a mechanism to intercept data requests.
36
+ * This is for use in testing.
37
+ *
38
+ * This component is not recommended for use in production code as it
39
+ * can prevent predictable functioning of the Wonder Blocks Data framework.
40
+ * One possible side-effect is that inflight requests from the interceptor could
41
+ * be picked up by `Data` component requests from outside the children of this
42
+ * component.
43
+ *
44
+ * Interceptions within the same component tree are chained such that the
45
+ * interceptor closest to the intercepted request is called first, and the
46
+ * furthest interceptor is called last.
47
+ */
48
+ const InterceptRequests = <TData: ValidCacheData>({
49
+ interceptor,
50
+ children,
51
+ }: Props<TData>): React.Node => {
52
+ const interceptors = React.useContext(InterceptContext);
53
+
54
+ const updatedInterceptors = React.useMemo(
55
+ // We could build this in reverse order so that our hook that does
56
+ // the interception didn't have to use reduceRight, but I think it
57
+ // is easier to think about if we do this in component tree order.
58
+ () => [...interceptors, interceptor],
59
+ [interceptors, interceptor],
60
+ );
61
+
62
+ return (
63
+ <InterceptContext.Provider value={updatedInterceptors}>
64
+ {children}
65
+ </InterceptContext.Provider>
66
+ );
67
+ };
68
+
69
+ export default InterceptRequests;
@@ -0,0 +1,54 @@
1
+ When you want to generate tests that check the loading state and
2
+ subsequent loaded state are working correctly for your uses of `Data` you can
3
+ use the `InterceptRequests` component. You can also use this component to
4
+ register request interceptors for any code that uses the `useRequestInterception`
5
+ hook.
6
+
7
+ This component takes the children to be rendered, and an interceptor function.
8
+
9
+ Note that this component is expected to be used only within test cases or
10
+ stories. Be careful want request IDs are matched to avoid intercepting the
11
+ wrong requests and remember that in-flight requests for a given request ID
12
+ can be shared - which means a bad request ID match could share requests across
13
+ different request IDs..
14
+
15
+ The `interceptor` intercept function has the form:
16
+
17
+ ```js static
18
+ (requestId: string) => ?Promise<?TData>;
19
+ ```
20
+
21
+ If this method returns `null`, then the next interceptor in the chain is
22
+ invoked, ultimately ending with the original handler. This
23
+ means that a request will be made for data via the handler assigned to the
24
+ `Data` component being intercepted if no interceptor handles the request first.
25
+
26
+ ```jsx
27
+ import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
28
+ import {View} from "@khanacademy/wonder-blocks-core";
29
+ import {InterceptRequests, Data} from "@khanacademy/wonder-blocks-data";
30
+ import {Strut} from "@khanacademy/wonder-blocks-layout";
31
+ import Color from "@khanacademy/wonder-blocks-color";
32
+ import Spacing from "@khanacademy/wonder-blocks-spacing";
33
+
34
+ const myHandler = () => Promise.reject(new Error("You should not see this!"));
35
+
36
+ const interceptor = (requestId) => requestId === "INTERCEPT_EXAMPLE" ? Promise.resolve("INTERCEPTED DATA!") : null;
37
+
38
+ <InterceptRequests interceptor={interceptor}>
39
+ <View>
40
+ <Body>This received intercepted data!</Body>
41
+ <Data handler={myHandler} requestId="INTERCEPT_EXAMPLE">
42
+ {(result) => {
43
+ if (result.status !== "success") {
44
+ return "If you see this, the example is broken!";
45
+ }
46
+
47
+ return (
48
+ <BodyMonospace>{result.data}</BodyMonospace>
49
+ );
50
+ }}
51
+ </Data>
52
+ </View>
53
+ </InterceptRequests>
54
+ ```
@@ -65,19 +65,11 @@ import {Strut} from "@khanacademy/wonder-blocks-layout";
65
65
  import Spacing from "@khanacademy/wonder-blocks-spacing";
66
66
  import Button from "@khanacademy/wonder-blocks-button";
67
67
  import {Server, View} from "@khanacademy/wonder-blocks-core";
68
- import {Data, TrackData, RequestHandler, fulfillAllDataRequests} from "@khanacademy/wonder-blocks-data";
68
+ import {Data, TrackData, fulfillAllDataRequests} from "@khanacademy/wonder-blocks-data";
69
69
 
70
- class MyPretendHandler extends RequestHandler {
71
- constructor() {
72
- super("MY_PRETEND_HANDLER");
73
- }
74
-
75
- fulfillRequest(options) {
76
- return new Promise((resolve, reject) =>
77
- setTimeout(() => resolve("DATA!"), 3000),
78
- );
79
- }
80
- }
70
+ const myPretendHandler = () => new Promise((resolve, reject) =>
71
+ setTimeout(() => resolve("DATA!"), 3000),
72
+ );
81
73
 
82
74
  class Example extends React.Component {
83
75
  constructor() {
@@ -87,7 +79,6 @@ class Example extends React.Component {
87
79
  * for the scope of this component.
88
80
  */
89
81
  this.state = {};
90
- this._handler = new MyPretendHandler();
91
82
  }
92
83
 
93
84
  static getDerivedStateFromError(error) {
@@ -133,15 +124,16 @@ class Example extends React.Component {
133
124
  const data = this.state.data
134
125
  ? JSON.stringify(this.state.data, undefined, " ")
135
126
  : "Data requested...";
127
+
136
128
  return (
137
129
  <React.Fragment>
138
130
  <Strut size={Spacing.small_12} />
139
131
  <TrackData>
140
- <Data handler={this._handler} options={{}}>
141
- {({loading, data, error}) => (
132
+ <Data handler={myPretendHandler} requestId="TRACK_DATA_EXAMPLE">
133
+ {(result) => (
142
134
  <View>
143
- <BodyMonospace>{`Loading: ${loading}`}</BodyMonospace>
144
- <BodyMonospace>{`Data: ${JSON.stringify(data)}`}</BodyMonospace>
135
+ <BodyMonospace>{`Loading: ${result.status === "loading"}`}</BodyMonospace>
136
+ <BodyMonospace>{`Data: ${JSON.stringify(result.data)}`}</BodyMonospace>
145
137
  </View>
146
138
  )}
147
139
  </Data>
@@ -162,12 +154,6 @@ class Example extends React.Component {
162
154
  rendered tree.
163
155
  </Body>
164
156
  <Strut size={Spacing.small_12} />
165
- <Body>
166
- If you click to remount after the data appears, we'll
167
- rerender with the now cached data, and above should
168
- update accordingly.
169
- </Body>
170
- <Strut size={Spacing.small_12} />
171
157
  <BodyMonospace>{data}</BodyMonospace>
172
158
  </View>
173
159
  </React.Fragment>
@@ -0,0 +1,17 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`#useSharedCache should throw if the id is 1`] = `[InvalidInputError: id must be a non-empty string]`;
4
+
5
+ exports[`#useSharedCache should throw if the id is [Function anonymous] 1`] = `[InvalidInputError: id must be a non-empty string]`;
6
+
7
+ exports[`#useSharedCache should throw if the id is 5 1`] = `[InvalidInputError: id must be a non-empty string]`;
8
+
9
+ exports[`#useSharedCache should throw if the id is null 1`] = `[InvalidInputError: id must be a non-empty string]`;
10
+
11
+ exports[`#useSharedCache should throw if the scope is 1`] = `[InvalidInputError: scope must be a non-empty string]`;
12
+
13
+ exports[`#useSharedCache should throw if the scope is [Function anonymous] 1`] = `[InvalidInputError: scope must be a non-empty string]`;
14
+
15
+ exports[`#useSharedCache should throw if the scope is 5 1`] = `[InvalidInputError: scope must be a non-empty string]`;
16
+
17
+ exports[`#useSharedCache should throw if the scope is null 1`] = `[InvalidInputError: scope must be a non-empty string]`;
@@ -80,6 +80,7 @@ describe("#useGql", () => {
80
80
  id: "MyQuery",
81
81
  };
82
82
  const gqlOpContext = {
83
+ a: undefined, // This should not get included.
83
84
  b: "overrideB",
84
85
  };
85
86
  const gqlOpVariables = {