@khanacademy/wonder-blocks-data 2.3.3 → 3.1.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 +21 -0
- package/dist/es/index.js +365 -429
- package/dist/index.js +455 -461
- package/docs.md +19 -13
- package/package.json +6 -6
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +40 -160
- package/src/__tests__/generated-snapshot.test.js +15 -195
- package/src/components/__tests__/data.test.js +159 -965
- package/src/components/__tests__/gql-router.test.js +64 -0
- package/src/components/__tests__/intercept-data.test.js +9 -66
- package/src/components/__tests__/track-data.test.js +6 -5
- package/src/components/data.js +9 -119
- package/src/components/data.md +38 -60
- package/src/components/gql-router.js +66 -0
- package/src/components/intercept-context.js +2 -3
- package/src/components/intercept-data.js +2 -34
- package/src/components/intercept-data.md +7 -105
- package/src/hooks/__tests__/use-data.test.js +826 -0
- package/src/hooks/__tests__/use-gql.test.js +233 -0
- package/src/hooks/use-data.js +143 -0
- package/src/hooks/use-gql.js +75 -0
- package/src/index.js +7 -9
- package/src/util/__tests__/get-gql-data-from-response.test.js +187 -0
- package/src/util/__tests__/memory-cache.test.js +134 -35
- package/src/util/__tests__/request-fulfillment.test.js +21 -36
- package/src/util/__tests__/request-handler.test.js +30 -30
- package/src/util/__tests__/request-tracking.test.js +29 -30
- package/src/util/__tests__/response-cache.test.js +521 -561
- package/src/util/__tests__/result-from-cache-entry.test.js +68 -0
- package/src/util/get-gql-data-from-response.js +69 -0
- package/src/util/gql-error.js +36 -0
- package/src/util/gql-router-context.js +6 -0
- package/src/util/gql-types.js +60 -0
- package/src/util/memory-cache.js +20 -15
- package/src/util/request-fulfillment.js +4 -0
- package/src/util/request-handler.js +4 -28
- package/src/util/request-handler.md +0 -32
- package/src/util/request-tracking.js +2 -3
- package/src/util/response-cache.js +50 -110
- package/src/util/result-from-cache-entry.js +38 -0
- package/src/util/types.js +14 -35
- package/LICENSE +0 -21
- package/src/components/__tests__/intercept-cache.test.js +0 -124
- package/src/components/__tests__/internal-data.test.js +0 -1030
- package/src/components/intercept-cache.js +0 -79
- package/src/components/intercept-cache.md +0 -103
- package/src/components/internal-data.js +0 -219
- package/src/util/__tests__/no-cache.test.js +0 -112
- package/src/util/no-cache.js +0 -66
- package/src/util/no-cache.md +0 -66
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {render} from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
import {GqlRouterContext} from "../../util/gql-router-context.js";
|
|
6
|
+
import {GqlRouter} from "../gql-router.js";
|
|
7
|
+
|
|
8
|
+
describe("GqlRouter", () => {
|
|
9
|
+
it("should provide the GqlRouterContext as configured", async () => {
|
|
10
|
+
// Arrange
|
|
11
|
+
const defaultContext = {
|
|
12
|
+
foo: "bar",
|
|
13
|
+
};
|
|
14
|
+
const fetch = jest.fn();
|
|
15
|
+
const CaptureContext = ({captureFn}) => {
|
|
16
|
+
captureFn(React.useContext(GqlRouterContext));
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Act
|
|
21
|
+
const result = await new Promise((resolve, reject) => {
|
|
22
|
+
render(
|
|
23
|
+
<GqlRouter defaultContext={defaultContext} fetch={fetch}>
|
|
24
|
+
<CaptureContext captureFn={resolve} />
|
|
25
|
+
</GqlRouter>,
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Assert
|
|
30
|
+
expect(result).toStrictEqual({
|
|
31
|
+
defaultContext,
|
|
32
|
+
fetch,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should not render React.memo-ized children if props remain the same", () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
const defaultContext = {
|
|
39
|
+
foo: "bar",
|
|
40
|
+
};
|
|
41
|
+
const fetch = jest.fn();
|
|
42
|
+
let renderCount = 0;
|
|
43
|
+
const Child = React.memo(() => {
|
|
44
|
+
const context = React.useContext(GqlRouterContext);
|
|
45
|
+
renderCount++;
|
|
46
|
+
return <div>{JSON.stringify(context)}</div>;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Act
|
|
50
|
+
const {rerender} = render(
|
|
51
|
+
<GqlRouter defaultContext={defaultContext} fetch={fetch}>
|
|
52
|
+
<Child />
|
|
53
|
+
</GqlRouter>,
|
|
54
|
+
);
|
|
55
|
+
rerender(
|
|
56
|
+
<GqlRouter defaultContext={defaultContext} fetch={fetch}>
|
|
57
|
+
<Child />
|
|
58
|
+
</GqlRouter>,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Assert
|
|
62
|
+
expect(renderCount).toBe(1);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import {
|
|
3
|
+
import {render} from "@testing-library/react";
|
|
4
4
|
|
|
5
5
|
import InterceptContext from "../intercept-context.js";
|
|
6
6
|
import InterceptData from "../intercept-data.js";
|
|
7
|
-
import InterceptCache from "../intercept-cache.js";
|
|
8
7
|
|
|
9
8
|
import type {IRequestHandler} from "../../util/types.js";
|
|
10
9
|
|
|
@@ -13,31 +12,22 @@ describe("InterceptData", () => {
|
|
|
13
12
|
jest.resetAllMocks();
|
|
14
13
|
});
|
|
15
14
|
|
|
16
|
-
it
|
|
17
|
-
["with only fulfillRequest method", {fulfillRequest: jest.fn()}],
|
|
18
|
-
[
|
|
19
|
-
"with only shouldRefreshCache method",
|
|
20
|
-
{shouldRefreshCache: jest.fn()},
|
|
21
|
-
],
|
|
22
|
-
[
|
|
23
|
-
"with both fulfillRequest and shouldRefreshCache methods",
|
|
24
|
-
{fulfillRequest: jest.fn(), shouldRefreshCache: jest.fn()},
|
|
25
|
-
],
|
|
26
|
-
])("should update context %s", (_, props) => {
|
|
15
|
+
it("should update context with fulfillRequest method", () => {
|
|
27
16
|
// Arrange
|
|
28
17
|
const fakeHandler: IRequestHandler<string, string> = {
|
|
29
18
|
fulfillRequest: () => Promise.resolve("data"),
|
|
30
19
|
getKey: (o) => o,
|
|
31
|
-
shouldRefreshCache: () => false,
|
|
32
20
|
type: "MY_HANDLER",
|
|
33
|
-
cache: null,
|
|
34
21
|
hydrate: true,
|
|
35
22
|
};
|
|
36
|
-
props
|
|
23
|
+
const props = {
|
|
24
|
+
handler: fakeHandler,
|
|
25
|
+
fulfillRequest: jest.fn(),
|
|
26
|
+
};
|
|
37
27
|
const captureContextFn = jest.fn();
|
|
38
28
|
|
|
39
29
|
// Act
|
|
40
|
-
|
|
30
|
+
render(
|
|
41
31
|
<InterceptData {...props}>
|
|
42
32
|
<InterceptContext.Consumer>
|
|
43
33
|
{captureContextFn}
|
|
@@ -49,8 +39,7 @@ describe("InterceptData", () => {
|
|
|
49
39
|
expect(captureContextFn).toHaveBeenCalledWith(
|
|
50
40
|
expect.objectContaining({
|
|
51
41
|
MY_HANDLER: {
|
|
52
|
-
fulfillRequest: props.fulfillRequest
|
|
53
|
-
shouldRefreshCache: props.shouldRefreshCache || null,
|
|
42
|
+
fulfillRequest: props.fulfillRequest,
|
|
54
43
|
},
|
|
55
44
|
}),
|
|
56
45
|
);
|
|
@@ -61,28 +50,23 @@ describe("InterceptData", () => {
|
|
|
61
50
|
const fakeHandler: IRequestHandler<string, string> = {
|
|
62
51
|
fulfillRequest: () => Promise.resolve("data"),
|
|
63
52
|
getKey: (o) => o,
|
|
64
|
-
shouldRefreshCache: () => false,
|
|
65
53
|
type: "MY_HANDLER",
|
|
66
54
|
cache: null,
|
|
67
55
|
hydrate: true,
|
|
68
56
|
};
|
|
69
57
|
const fulfillRequest1Fn = jest.fn();
|
|
70
|
-
const shouldRefreshCache1Fn = jest.fn();
|
|
71
58
|
const fulfillRequest2Fn = jest.fn();
|
|
72
|
-
const shouldRefreshCache2Fn = jest.fn();
|
|
73
59
|
const captureContextFn = jest.fn();
|
|
74
60
|
|
|
75
61
|
// Act
|
|
76
|
-
|
|
62
|
+
render(
|
|
77
63
|
<InterceptData
|
|
78
64
|
handler={fakeHandler}
|
|
79
65
|
fulfillRequest={fulfillRequest1Fn}
|
|
80
|
-
shouldRefreshCache={shouldRefreshCache1Fn}
|
|
81
66
|
>
|
|
82
67
|
<InterceptData
|
|
83
68
|
handler={fakeHandler}
|
|
84
69
|
fulfillRequest={fulfillRequest2Fn}
|
|
85
|
-
shouldRefreshCache={shouldRefreshCache2Fn}
|
|
86
70
|
>
|
|
87
71
|
<InterceptContext.Consumer>
|
|
88
72
|
{captureContextFn}
|
|
@@ -96,47 +80,6 @@ describe("InterceptData", () => {
|
|
|
96
80
|
expect.objectContaining({
|
|
97
81
|
MY_HANDLER: {
|
|
98
82
|
fulfillRequest: fulfillRequest2Fn,
|
|
99
|
-
shouldRefreshCache: shouldRefreshCache2Fn,
|
|
100
|
-
},
|
|
101
|
-
}),
|
|
102
|
-
);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("should not change InterceptCache methods on existing interceptor", () => {
|
|
106
|
-
// Arrange
|
|
107
|
-
const fakeHandler: IRequestHandler<string, string> = {
|
|
108
|
-
fulfillRequest: () => Promise.resolve("data"),
|
|
109
|
-
getKey: (o) => o,
|
|
110
|
-
shouldRefreshCache: () => false,
|
|
111
|
-
type: "MY_HANDLER",
|
|
112
|
-
cache: null,
|
|
113
|
-
hydrate: true,
|
|
114
|
-
};
|
|
115
|
-
const fulfillRequestFn = jest.fn();
|
|
116
|
-
const getEntryFn = jest.fn();
|
|
117
|
-
const captureContextFn = jest.fn();
|
|
118
|
-
|
|
119
|
-
// Act
|
|
120
|
-
mount(
|
|
121
|
-
<InterceptCache handler={fakeHandler} getEntry={getEntryFn}>
|
|
122
|
-
<InterceptData
|
|
123
|
-
handler={fakeHandler}
|
|
124
|
-
fulfillRequest={fulfillRequestFn}
|
|
125
|
-
>
|
|
126
|
-
<InterceptContext.Consumer>
|
|
127
|
-
{captureContextFn}
|
|
128
|
-
</InterceptContext.Consumer>
|
|
129
|
-
</InterceptData>
|
|
130
|
-
</InterceptCache>,
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
// Assert
|
|
134
|
-
expect(captureContextFn).toHaveBeenCalledWith(
|
|
135
|
-
expect.objectContaining({
|
|
136
|
-
MY_HANDLER: {
|
|
137
|
-
fulfillRequest: fulfillRequestFn,
|
|
138
|
-
shouldRefreshCache: null,
|
|
139
|
-
getEntry: getEntryFn,
|
|
140
83
|
},
|
|
141
84
|
}),
|
|
142
85
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
4
|
-
import {
|
|
4
|
+
import {render, screen} from "@testing-library/react";
|
|
5
5
|
|
|
6
6
|
import TrackData from "../track-data.js";
|
|
7
7
|
import {RequestTracker, TrackerContext} from "../../util/request-tracking.js";
|
|
@@ -16,7 +16,7 @@ describe("TrackData", () => {
|
|
|
16
16
|
jest.spyOn(Server, "isServerSide").mockReturnValue(false);
|
|
17
17
|
|
|
18
18
|
// Act
|
|
19
|
-
const underTest = () =>
|
|
19
|
+
const underTest = () => render(<TrackData>SOME CHILDREN</TrackData>);
|
|
20
20
|
|
|
21
21
|
// Assert
|
|
22
22
|
expect(underTest).toThrowErrorMatchingInlineSnapshot(
|
|
@@ -29,10 +29,11 @@ describe("TrackData", () => {
|
|
|
29
29
|
jest.spyOn(Server, "isServerSide").mockReturnValue(true);
|
|
30
30
|
|
|
31
31
|
// Act
|
|
32
|
-
|
|
32
|
+
render(<TrackData>SOME CHILDREN</TrackData>);
|
|
33
|
+
const result = screen.getByText("SOME CHILDREN");
|
|
33
34
|
|
|
34
35
|
// Assert
|
|
35
|
-
expect(result).
|
|
36
|
+
expect(result).toBeInTheDocument();
|
|
36
37
|
});
|
|
37
38
|
|
|
38
39
|
it("should provide tracker function for tracking context", async () => {
|
|
@@ -41,7 +42,7 @@ describe("TrackData", () => {
|
|
|
41
42
|
|
|
42
43
|
// Act
|
|
43
44
|
const result = await new Promise((resolve, reject) => {
|
|
44
|
-
|
|
45
|
+
render(
|
|
45
46
|
<TrackData>
|
|
46
47
|
<TrackerContext.Consumer>
|
|
47
48
|
{(fn) => resolve(fn)}
|
package/src/components/data.js
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import InternalData from "./internal-data.js";
|
|
4
|
+
import {useData} from "../hooks/use-data.js";
|
|
6
5
|
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
CacheEntry,
|
|
11
|
-
Interceptor,
|
|
12
|
-
Result,
|
|
13
|
-
IRequestHandler,
|
|
14
|
-
ValidData,
|
|
15
|
-
} from "../util/types.js";
|
|
6
|
+
import type {Result, IRequestHandler, ValidData} from "../util/types.js";
|
|
16
7
|
|
|
17
8
|
type Props<
|
|
18
9
|
/**
|
|
@@ -54,111 +45,10 @@ type Props<
|
|
|
54
45
|
* requirements can be placed in a React application in a manner that will
|
|
55
46
|
* support server-side rendering and efficient caching.
|
|
56
47
|
*/
|
|
57
|
-
|
|
58
|
-
Props<TOptions, TData>,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!interceptor) {
|
|
65
|
-
return handler;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const {fulfillRequest, shouldRefreshCache} = interceptor;
|
|
69
|
-
const fulfillRequestFn = fulfillRequest
|
|
70
|
-
? (options: TOptions): Promise<TData> => {
|
|
71
|
-
const interceptedResult = fulfillRequest(options);
|
|
72
|
-
return interceptedResult != null
|
|
73
|
-
? interceptedResult
|
|
74
|
-
: handler.fulfillRequest(options);
|
|
75
|
-
}
|
|
76
|
-
: (options) => handler.fulfillRequest(options);
|
|
77
|
-
const shouldRefreshCacheFn = shouldRefreshCache
|
|
78
|
-
? (
|
|
79
|
-
options: TOptions,
|
|
80
|
-
cacheEntry: ?$ReadOnly<CacheEntry<TData>>,
|
|
81
|
-
): boolean => {
|
|
82
|
-
const interceptedResult = shouldRefreshCache(
|
|
83
|
-
options,
|
|
84
|
-
cacheEntry,
|
|
85
|
-
);
|
|
86
|
-
return interceptedResult != null
|
|
87
|
-
? interceptedResult
|
|
88
|
-
: handler.shouldRefreshCache(options, cacheEntry);
|
|
89
|
-
}
|
|
90
|
-
: (options, cacheEntry) =>
|
|
91
|
-
handler.shouldRefreshCache(options, cacheEntry);
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
fulfillRequest: fulfillRequestFn,
|
|
95
|
-
shouldRefreshCache: shouldRefreshCacheFn,
|
|
96
|
-
getKey: (options) => handler.getKey(options),
|
|
97
|
-
type: handler.type,
|
|
98
|
-
cache: handler.cache,
|
|
99
|
-
hydrate: handler.hydrate,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
_getCacheLookupFnFromInterceptor(
|
|
104
|
-
interceptor: ?Interceptor,
|
|
105
|
-
): $PropertyType<ResponseCache, "getEntry"> {
|
|
106
|
-
const getEntry = interceptor && interceptor.getEntry;
|
|
107
|
-
if (!getEntry) {
|
|
108
|
-
return ResponseCache.Default.getEntry;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return <TOptions, TData: ValidData>(
|
|
112
|
-
handler: IRequestHandler<TOptions, TData>,
|
|
113
|
-
options: TOptions,
|
|
114
|
-
): ?$ReadOnly<CacheEntry<TData>> => {
|
|
115
|
-
// 1. Lookup the current cache value.
|
|
116
|
-
const cacheEntry = ResponseCache.Default.getEntry<TOptions, TData>(
|
|
117
|
-
handler,
|
|
118
|
-
options,
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
// 2. See if our interceptor wants to override it.
|
|
122
|
-
const interceptedData = getEntry(options, cacheEntry);
|
|
123
|
-
|
|
124
|
-
// 3. Return the appropriate response.
|
|
125
|
-
return interceptedData != null ? interceptedData : cacheEntry;
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
render(): React.Node {
|
|
130
|
-
return (
|
|
131
|
-
<InterceptContext.Consumer>
|
|
132
|
-
{(value) => {
|
|
133
|
-
const handlerType = this.props.handler.type;
|
|
134
|
-
const interceptor = value[handlerType];
|
|
135
|
-
const handler = this._getHandlerFromInterceptor(
|
|
136
|
-
interceptor,
|
|
137
|
-
);
|
|
138
|
-
const getEntry = this._getCacheLookupFnFromInterceptor(
|
|
139
|
-
interceptor,
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Need to share our types with InternalData so Flow
|
|
144
|
-
* doesn't need to infer them and find mismatches.
|
|
145
|
-
* However, just deriving a new component creates issues
|
|
146
|
-
* where InternalData starts rerendering too often.
|
|
147
|
-
* Couldn't track down why, so suppressing the error
|
|
148
|
-
* instead.
|
|
149
|
-
*/
|
|
150
|
-
return (
|
|
151
|
-
<InternalData
|
|
152
|
-
// $FlowIgnore[incompatible-type-arg]
|
|
153
|
-
handler={handler}
|
|
154
|
-
options={this.props.options}
|
|
155
|
-
getEntry={getEntry}
|
|
156
|
-
>
|
|
157
|
-
{(result) => this.props.children(result)}
|
|
158
|
-
</InternalData>
|
|
159
|
-
);
|
|
160
|
-
}}
|
|
161
|
-
</InterceptContext.Consumer>
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
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);
|
|
53
|
+
};
|
|
54
|
+
export default Data;
|
package/src/components/data.md
CHANGED
|
@@ -3,34 +3,36 @@ most folks will use. It describes a data requirement in terms of a handler, and
|
|
|
3
3
|
some options. Handlers must implement the
|
|
4
4
|
`IRequestHandler` interface.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
asked to do so.
|
|
6
|
+
The handler is responsible for fulfilling the request when asked to do so.
|
|
8
7
|
|
|
9
|
-
####
|
|
8
|
+
#### Server-side Rendering and Hydration
|
|
10
9
|
|
|
11
|
-
The Wonder Blocks Data framework
|
|
12
|
-
|
|
13
|
-
obtained during SSR to support hydration of the SSR result.
|
|
10
|
+
The Wonder Blocks Data framework uses an in-memory cache for supporting
|
|
11
|
+
server-side rendering (SSR) and hydration.
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
handler. Custom caches allow for different client-side caching
|
|
17
|
-
strategies (such as using local storage instead of memory). Any custom cache
|
|
18
|
-
provided is ignored during SSR.
|
|
13
|
+
##### Server-side behavior
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
entry. If the custom cache returns an entry, that is used. If the custom cache
|
|
22
|
-
returns `null`, the framework will look for a corresponding in-memory entry with
|
|
23
|
-
which the framework has been initialzied, and if there, store the entry in the
|
|
24
|
-
custom cache and then return it.
|
|
15
|
+
###### Cache miss
|
|
25
16
|
|
|
26
|
-
|
|
27
|
-
|
|
17
|
+
When the `Data` component does not get data or an error from the cache and it
|
|
18
|
+
is rendering server-side, it tells our request tracking that it wants data, and
|
|
19
|
+
it renders in its `loading` state. It will always render in this state if there
|
|
20
|
+
is no cached response.
|
|
28
21
|
|
|
29
|
-
|
|
22
|
+
###### Cache hit
|
|
30
23
|
|
|
31
|
-
|
|
24
|
+
When the `Data` component gets data or an error from the cache and it is
|
|
25
|
+
rendering server-side, it will render as loaded, with that data or error,
|
|
26
|
+
as it would client-side. In this situation, it does not track the request it
|
|
27
|
+
would have made, as it already has the data and doesn't need to.
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
|
|
30
|
+
##### Client-side behavior
|
|
31
|
+
|
|
32
|
+
###### Cache miss
|
|
33
|
+
|
|
34
|
+
When the hydration cache does not contain the data, the data will be requested.
|
|
35
|
+
While the request is pending, the data is rendered in the loading state.
|
|
34
36
|
In this example, we use a 3 second delayed promise to simulate the request.
|
|
35
37
|
We start out without any data and so the request is made. Upon receipt of that
|
|
36
38
|
data or an error, we re-render.
|
|
@@ -103,21 +105,16 @@ const invalid = new MyInvalidHandler();
|
|
|
103
105
|
</View>
|
|
104
106
|
```
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
If the cache already contains data or an error for our request, then the `Data`
|
|
109
|
-
component will render it immediately. The cache data is placed there either
|
|
110
|
-
by prior successful requests as in the above Cache Miss example, or via calling
|
|
111
|
-
`initializeCache` before any requests have been made. A cache hit may also
|
|
112
|
-
occur due to the use of the `InterceptCache` component.
|
|
108
|
+
###### Cache hit
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
If the hydration cache already contains data or an error for our request, then
|
|
111
|
+
the `Data` component will render it immediately. The hydration cache is
|
|
112
|
+
populated using the `initializeCache` method before rendering.
|
|
116
113
|
|
|
117
114
|
```jsx
|
|
118
115
|
import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
|
|
119
116
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
120
|
-
import {
|
|
117
|
+
import {Data, RequestHandler, initializeCache} from "@khanacademy/wonder-blocks-data";
|
|
121
118
|
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
122
119
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
123
120
|
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
@@ -135,24 +132,21 @@ class MyHandler extends RequestHandler {
|
|
|
135
132
|
"If you're seeing this error, the examples are broken and data isn't in the cache that should be.",
|
|
136
133
|
);
|
|
137
134
|
}
|
|
138
|
-
|
|
139
|
-
shouldRefreshCache(options, cachedEntry) {
|
|
140
|
-
/**
|
|
141
|
-
* For our purposes, the cache never needs a refresh.
|
|
142
|
-
*/
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
135
|
}
|
|
146
136
|
|
|
147
137
|
const handler = new MyHandler();
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
138
|
+
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
|
+
}
|
|
151
146
|
}
|
|
152
|
-
|
|
153
|
-
};
|
|
147
|
+
});
|
|
154
148
|
|
|
155
|
-
<
|
|
149
|
+
<View>
|
|
156
150
|
<View>
|
|
157
151
|
<Body>This cache has data!</Body>
|
|
158
152
|
<Data handler={handler} options={"DATA"}>
|
|
@@ -182,21 +176,5 @@ const getEntryInterceptor = function(options) {
|
|
|
182
176
|
}}
|
|
183
177
|
</Data>
|
|
184
178
|
</View>
|
|
185
|
-
</
|
|
179
|
+
</View>
|
|
186
180
|
```
|
|
187
|
-
|
|
188
|
-
#### Server-side behavior
|
|
189
|
-
|
|
190
|
-
##### Cache miss
|
|
191
|
-
|
|
192
|
-
When the `Data` component does not get data or an error from the cache and it
|
|
193
|
-
is rendering server-side, it tells our request tracking that it wants data, and
|
|
194
|
-
it renders in its `loading` state. It will always render in this state if there
|
|
195
|
-
is no cached response.
|
|
196
|
-
|
|
197
|
-
##### Cache hit
|
|
198
|
-
|
|
199
|
-
When the `Data` component gets data or an error from the cache and it is
|
|
200
|
-
rendering server-side, it will render as loaded, with that data or error,
|
|
201
|
-
as it would client-side. In this situation, it does not track the request it
|
|
202
|
-
would have made, as it already has the data and doesn't need to.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import {GqlRouterContext} from "../util/gql-router-context.js";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
GqlContext,
|
|
8
|
+
FetchFn,
|
|
9
|
+
GqlRouterConfiguration,
|
|
10
|
+
} from "../util/gql-types.js";
|
|
11
|
+
|
|
12
|
+
type Props<TContext: GqlContext> = {|
|
|
13
|
+
/**
|
|
14
|
+
* The default context to be used by operations when no context is provided.
|
|
15
|
+
*/
|
|
16
|
+
defaultContext: TContext,
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The function to use when fetching requests.
|
|
20
|
+
*/
|
|
21
|
+
fetch: FetchFn<any, any, any, TContext>,
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The children to be rendered inside the router.
|
|
25
|
+
*/
|
|
26
|
+
children: React.Node,
|
|
27
|
+
|};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configure GraphQL routing for GraphQL hooks and components.
|
|
31
|
+
*
|
|
32
|
+
* These can be nested. Components and hooks relying on the GraphQL routing
|
|
33
|
+
* will use the configuration from their closest ancestral GqlRouter.
|
|
34
|
+
*/
|
|
35
|
+
export const GqlRouter = <TContext: GqlContext>({
|
|
36
|
+
defaultContext: thisDefaultContext,
|
|
37
|
+
fetch: thisFetch,
|
|
38
|
+
children,
|
|
39
|
+
}: Props<TContext>): React.Node => {
|
|
40
|
+
// We don't care if we're nested. We always force our callers to define
|
|
41
|
+
// everything. It makes for a clearer API and requires less error checking
|
|
42
|
+
// code (assuming our flow types are correct). We also don't default fetch
|
|
43
|
+
// to anything - our callers can tell us what function to use quite easily.
|
|
44
|
+
// If code that consumes this wants more nuanced nesting, it can implement
|
|
45
|
+
// it within its own GqlRouter than then defers to this one.
|
|
46
|
+
|
|
47
|
+
// We want to always use the same object if things haven't changed to avoid
|
|
48
|
+
// over-rendering consumers of our context, let's memoize the configuration.
|
|
49
|
+
// By doing this, if a component under children that uses this context
|
|
50
|
+
// uses React.memo, we won't force it to re-render every time we render
|
|
51
|
+
// because we'll only change the context value if something has actually
|
|
52
|
+
// changed.
|
|
53
|
+
const configuration: GqlRouterConfiguration<TContext> = React.useMemo(
|
|
54
|
+
() => ({
|
|
55
|
+
fetch: thisFetch,
|
|
56
|
+
defaultContext: thisDefaultContext,
|
|
57
|
+
}),
|
|
58
|
+
[thisDefaultContext, thisFetch],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<GqlRouterContext.Provider value={configuration}>
|
|
63
|
+
{children}
|
|
64
|
+
</GqlRouterContext.Provider>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -8,8 +8,7 @@ import type {InterceptContextData} from "../util/types.js";
|
|
|
8
8
|
*
|
|
9
9
|
* INTERNAL USE ONLY
|
|
10
10
|
*/
|
|
11
|
-
const InterceptContext: React.Context<InterceptContextData> =
|
|
12
|
-
{}
|
|
13
|
-
);
|
|
11
|
+
const InterceptContext: React.Context<InterceptContextData> =
|
|
12
|
+
React.createContext<InterceptContextData>({});
|
|
14
13
|
|
|
15
14
|
export default InterceptContext;
|
|
@@ -7,10 +7,9 @@ import type {
|
|
|
7
7
|
ValidData,
|
|
8
8
|
IRequestHandler,
|
|
9
9
|
InterceptFulfillRequestFn,
|
|
10
|
-
InterceptShouldRefreshCacheFn,
|
|
11
10
|
} from "../util/types.js";
|
|
12
11
|
|
|
13
|
-
type
|
|
12
|
+
type Props<TOptions, TData> = {|
|
|
14
13
|
/**
|
|
15
14
|
* A handler of the type to be intercepted.
|
|
16
15
|
*/
|
|
@@ -24,9 +23,7 @@ type BaseProps<TOptions, TData> = {|
|
|
|
24
23
|
* one).
|
|
25
24
|
*/
|
|
26
25
|
children: React.Node,
|
|
27
|
-
|};
|
|
28
26
|
|
|
29
|
-
type FulfillRequestProps<TOptions, TData> = {|
|
|
30
27
|
/**
|
|
31
28
|
* Called to fulfill a request.
|
|
32
29
|
* If this returns null, the request will be fulfilled by the
|
|
@@ -35,38 +32,11 @@ type FulfillRequestProps<TOptions, TData> = {|
|
|
|
35
32
|
fulfillRequest: InterceptFulfillRequestFn<TOptions, TData>,
|
|
36
33
|
|};
|
|
37
34
|
|
|
38
|
-
type ShouldRefreshCacheProps<TOptions, TData> = {|
|
|
39
|
-
/**
|
|
40
|
-
* Called to determine if the cache should be refreshed.
|
|
41
|
-
* If this returns null, the handler being intercepted will be asked if
|
|
42
|
-
* the cache should be refreshed.
|
|
43
|
-
*/
|
|
44
|
-
shouldRefreshCache: InterceptShouldRefreshCacheFn<TOptions, TData>,
|
|
45
|
-
|};
|
|
46
|
-
|
|
47
|
-
type Props<TOptions, TData> =
|
|
48
|
-
| {|
|
|
49
|
-
...BaseProps<TOptions, TData>,
|
|
50
|
-
...FulfillRequestProps<TOptions, TData>,
|
|
51
|
-
...ShouldRefreshCacheProps<TOptions, TData>,
|
|
52
|
-
|}
|
|
53
|
-
| {|
|
|
54
|
-
...BaseProps<TOptions, TData>,
|
|
55
|
-
...FulfillRequestProps<TOptions, TData>,
|
|
56
|
-
|}
|
|
57
|
-
| {|
|
|
58
|
-
...BaseProps<TOptions, TData>,
|
|
59
|
-
...ShouldRefreshCacheProps<TOptions, TData>,
|
|
60
|
-
|};
|
|
61
|
-
|
|
62
35
|
/**
|
|
63
36
|
* This component provides a mechanism to intercept the data requests for the
|
|
64
37
|
* type of a given handler and provide alternative results. This is mostly
|
|
65
38
|
* useful for testing.
|
|
66
39
|
*
|
|
67
|
-
* Results from this interceptor will end up in the cache. If you
|
|
68
|
-
* wish to only override the cache, use `InterceptCache` instead.
|
|
69
|
-
*
|
|
70
40
|
* This component is not recommended for use in production code as it
|
|
71
41
|
* can prevent predictable functioning of the Wonder Blocks Data framework.
|
|
72
42
|
* One possible side-effect is that inflight requests from the interceptor could
|
|
@@ -89,9 +59,7 @@ export default class InterceptData<
|
|
|
89
59
|
const handlerType = this.props.handler.type;
|
|
90
60
|
const interceptor = {
|
|
91
61
|
...value[handlerType],
|
|
92
|
-
fulfillRequest: this.props.fulfillRequest
|
|
93
|
-
shouldRefreshCache:
|
|
94
|
-
this.props.shouldRefreshCache || null,
|
|
62
|
+
fulfillRequest: this.props.fulfillRequest,
|
|
95
63
|
};
|
|
96
64
|
const newValue = {
|
|
97
65
|
...value,
|