@khanacademy/wonder-blocks-data 13.0.10 → 13.0.12

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 (66) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +3 -3
  3. package/src/components/__tests__/data.test.tsx +0 -832
  4. package/src/components/__tests__/gql-router.test.tsx +0 -63
  5. package/src/components/__tests__/intercept-requests.test.tsx +0 -57
  6. package/src/components/__tests__/track-data.test.tsx +0 -56
  7. package/src/components/data.ts +0 -73
  8. package/src/components/gql-router.tsx +0 -63
  9. package/src/components/intercept-context.ts +0 -19
  10. package/src/components/intercept-requests.tsx +0 -67
  11. package/src/components/track-data.tsx +0 -28
  12. package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.ts.snap +0 -17
  13. package/src/hooks/__tests__/use-cached-effect.test.tsx +0 -789
  14. package/src/hooks/__tests__/use-gql-router-context.test.tsx +0 -132
  15. package/src/hooks/__tests__/use-gql.test.tsx +0 -204
  16. package/src/hooks/__tests__/use-hydratable-effect.test.ts +0 -708
  17. package/src/hooks/__tests__/use-request-interception.test.tsx +0 -254
  18. package/src/hooks/__tests__/use-server-effect.test.ts +0 -293
  19. package/src/hooks/__tests__/use-shared-cache.test.ts +0 -263
  20. package/src/hooks/use-cached-effect.ts +0 -297
  21. package/src/hooks/use-gql-router-context.ts +0 -49
  22. package/src/hooks/use-gql.ts +0 -58
  23. package/src/hooks/use-hydratable-effect.ts +0 -201
  24. package/src/hooks/use-request-interception.ts +0 -53
  25. package/src/hooks/use-server-effect.ts +0 -75
  26. package/src/hooks/use-shared-cache.ts +0 -107
  27. package/src/index.ts +0 -46
  28. package/src/util/__tests__/__snapshots__/scoped-in-memory-cache.test.ts.snap +0 -19
  29. package/src/util/__tests__/__snapshots__/serializable-in-memory-cache.test.ts.snap +0 -19
  30. package/src/util/__tests__/get-gql-data-from-response.test.ts +0 -186
  31. package/src/util/__tests__/get-gql-request-id.test.ts +0 -132
  32. package/src/util/__tests__/graphql-document-node-parser.test.ts +0 -535
  33. package/src/util/__tests__/hydration-cache-api.test.ts +0 -34
  34. package/src/util/__tests__/merge-gql-context.test.ts +0 -73
  35. package/src/util/__tests__/purge-caches.test.ts +0 -28
  36. package/src/util/__tests__/request-api.test.ts +0 -176
  37. package/src/util/__tests__/request-fulfillment.test.ts +0 -146
  38. package/src/util/__tests__/request-tracking.test.tsx +0 -321
  39. package/src/util/__tests__/result-from-cache-response.test.ts +0 -79
  40. package/src/util/__tests__/scoped-in-memory-cache.test.ts +0 -316
  41. package/src/util/__tests__/serializable-in-memory-cache.test.ts +0 -397
  42. package/src/util/__tests__/ssr-cache.test.ts +0 -636
  43. package/src/util/__tests__/to-gql-operation.test.ts +0 -41
  44. package/src/util/data-error.ts +0 -63
  45. package/src/util/get-gql-data-from-response.ts +0 -65
  46. package/src/util/get-gql-request-id.ts +0 -106
  47. package/src/util/gql-error.ts +0 -43
  48. package/src/util/gql-router-context.ts +0 -9
  49. package/src/util/gql-types.ts +0 -64
  50. package/src/util/graphql-document-node-parser.ts +0 -132
  51. package/src/util/graphql-types.ts +0 -28
  52. package/src/util/hydration-cache-api.ts +0 -30
  53. package/src/util/merge-gql-context.ts +0 -35
  54. package/src/util/purge-caches.ts +0 -14
  55. package/src/util/request-api.ts +0 -65
  56. package/src/util/request-fulfillment.ts +0 -121
  57. package/src/util/request-tracking.ts +0 -211
  58. package/src/util/result-from-cache-response.ts +0 -30
  59. package/src/util/scoped-in-memory-cache.ts +0 -121
  60. package/src/util/serializable-in-memory-cache.ts +0 -44
  61. package/src/util/ssr-cache.ts +0 -193
  62. package/src/util/status.ts +0 -35
  63. package/src/util/to-gql-operation.ts +0 -43
  64. package/src/util/types.ts +0 -145
  65. package/tsconfig-build.json +0 -12
  66. package/tsconfig-build.tsbuildinfo +0 -1
@@ -1,263 +0,0 @@
1
- // eslint-disable-next-line import/no-unassigned-import
2
- import "jest-extended";
3
- import {renderHook as clientRenderHook} from "@testing-library/react-hooks";
4
-
5
- import {useSharedCache, SharedCache} from "../use-shared-cache";
6
-
7
- describe("#useSharedCache", () => {
8
- beforeEach(() => {
9
- SharedCache.purgeAll();
10
- });
11
-
12
- it.each`
13
- id
14
- ${null}
15
- ${""}
16
- ${5}
17
- ${() => "BOO"}
18
- `("should throw if the id is $id", ({id}: any) => {
19
- // Arrange
20
-
21
- // Act
22
- const {result} = clientRenderHook(() => useSharedCache(id, "scope"));
23
-
24
- // Assert
25
- expect(result.error).toMatchSnapshot();
26
- });
27
-
28
- it.each`
29
- scope
30
- ${null}
31
- ${""}
32
- ${5}
33
- ${() => "BOO"}
34
- `("should throw if the scope is $scope", ({scope}: any) => {
35
- // Arrange
36
-
37
- // Act
38
- const {result} = clientRenderHook(() => useSharedCache("id", scope));
39
-
40
- // Assert
41
- expect(result.error).toMatchSnapshot();
42
- });
43
-
44
- it("should return a tuple of two items", () => {
45
- // Arrange
46
-
47
- // Act
48
- const {
49
- result: {current: result},
50
- } = clientRenderHook(() => useSharedCache("id", "scope"));
51
-
52
- // Assert
53
- expect(result).toBeArrayOfSize(2);
54
- });
55
-
56
- describe("tuple[0] - currentValue", () => {
57
- it("should be null if nothing is cached", () => {
58
- // Arrange
59
-
60
- // Act
61
- const {
62
- result: {current: result},
63
- } = clientRenderHook(() => useSharedCache("id", "scope"));
64
-
65
- // Assert
66
- expect(result[0]).toBeNull();
67
- });
68
-
69
- it("should match initialValue when provided as a non-function", () => {
70
- // Arrange
71
-
72
- // Act
73
- const {
74
- result: {current: result},
75
- } = clientRenderHook(() =>
76
- useSharedCache("id", "scope", "INITIAL VALUE"),
77
- );
78
-
79
- // Assert
80
- expect(result[0]).toBe("INITIAL VALUE");
81
- });
82
-
83
- it("should match the return of initialValue when provided as non-function", () => {
84
- // Arrange
85
-
86
- // Act
87
- const {
88
- result: {current: result},
89
- } = clientRenderHook(() =>
90
- useSharedCache("id", "scope", () => "INITIAL VALUE"),
91
- );
92
-
93
- // Assert
94
- expect(result[0]).toBe("INITIAL VALUE");
95
- });
96
- });
97
-
98
- describe("tuple[1] - setValue", () => {
99
- it("should be a function", () => {
100
- // Arrange
101
-
102
- // Act
103
- const {
104
- result: {current: result},
105
- } = clientRenderHook(() => useSharedCache("id", "scope"));
106
-
107
- // Assert
108
- expect(result[1]).toBeFunction();
109
- });
110
-
111
- it("should be the same function if the id and scope remain the same", () => {
112
- // Arrange
113
- const wrapper = clientRenderHook(
114
- ({id, scope}: any) => useSharedCache(id, scope),
115
- {initialProps: {id: "id", scope: "scope"}},
116
- );
117
-
118
- // Act
119
- wrapper.rerender({
120
- id: "id",
121
- scope: "scope",
122
- });
123
- const value1 = wrapper.result.all[wrapper.result.all.length - 2];
124
- const value2 = wrapper.result.current;
125
- const result1 = Array.isArray(value1) ? value1[1] : "BAD1";
126
- const result2 = Array.isArray(value2) ? value2[1] : "BAD2";
127
-
128
- // Assert
129
- expect(result1).toBe(result2);
130
- });
131
-
132
- it("should be a new function if the id changes", () => {
133
- // Arrange
134
- const wrapper = clientRenderHook(
135
- ({id}: any) => useSharedCache(id, "scope"),
136
- {
137
- initialProps: {id: "id"},
138
- },
139
- );
140
-
141
- // Act
142
- wrapper.rerender({id: "new-id"});
143
- const value1 = wrapper.result.all[wrapper.result.all.length - 2];
144
- const value2 = wrapper.result.current;
145
- const result1 = Array.isArray(value1) ? value1[1] : "BAD1";
146
- const result2 = Array.isArray(value2) ? value2[1] : "BAD2";
147
-
148
- // Assert
149
- expect(result1).not.toBe(result2);
150
- });
151
-
152
- it("should be a new function if the scope changes", () => {
153
- // Arrange
154
- const wrapper = clientRenderHook(
155
- ({scope}: any) => useSharedCache("id", scope),
156
- {
157
- initialProps: {scope: "scope"},
158
- },
159
- );
160
-
161
- // Act
162
- wrapper.rerender({scope: "new-scope"});
163
- const value1 = wrapper.result.all[wrapper.result.all.length - 2];
164
- const value2 = wrapper.result.current;
165
- const result1 = Array.isArray(value1) ? value1[1] : "BAD1";
166
- const result2 = Array.isArray(value2) ? value2[1] : "BAD2";
167
-
168
- // Assert
169
- expect(result1).not.toBe(result2);
170
- });
171
-
172
- it("should set the value in the cache", () => {
173
- // Arrange
174
- const wrapper = clientRenderHook(() =>
175
- useSharedCache("id", "scope"),
176
- );
177
- const setValue = wrapper.result.current[1];
178
-
179
- // Act
180
- setValue("CACHED_VALUE");
181
- // Rerender so the hook retrieves this new value.
182
- wrapper.rerender();
183
- const result = wrapper.result.current[0];
184
-
185
- // Assert
186
- expect(result).toBe("CACHED_VALUE");
187
- });
188
-
189
- it.each`
190
- value
191
- ${undefined}
192
- ${null}
193
- `("should purge the value from the cache if $value", ({value}: any) => {
194
- // Arrange
195
- const wrapper = clientRenderHook(() =>
196
- useSharedCache("id", "scope"),
197
- );
198
- const setValue = wrapper.result.current[1];
199
- setValue("CACHED_VALUE");
200
-
201
- // Act
202
- // Rerender so the result has the cached value.
203
- wrapper.rerender();
204
- setValue(value);
205
- // Rerender so the hook retrieves this new value.
206
- wrapper.rerender();
207
- const result = wrapper.result.current[0];
208
-
209
- // Assert
210
- expect(result).toBeNull();
211
- });
212
- });
213
-
214
- it("should share cache across all uses", () => {
215
- // Arrange
216
- const hook1 = clientRenderHook(() => useSharedCache("id", "scope"));
217
- const hook2 = clientRenderHook(() => useSharedCache("id", "scope"));
218
- hook1.result.current[1]("VALUE_1");
219
-
220
- // Act
221
- hook2.rerender();
222
- const result = hook2.result.current[0];
223
-
224
- // Assert
225
- expect(result).toBe("VALUE_1");
226
- });
227
-
228
- it.each`
229
- id
230
- ${"id1"}
231
- ${"id2"}
232
- `("should not share cache if scope is different", ({id}: any) => {
233
- // Arrange
234
- const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
235
- const hook2 = clientRenderHook(() => useSharedCache(id, "scope2"));
236
- hook1.result.current[1]("VALUE_1");
237
-
238
- // Act
239
- hook2.rerender();
240
- const result = hook2.result.current[0];
241
-
242
- // Assert
243
- expect(result).toBeNull();
244
- });
245
-
246
- it.each`
247
- scope
248
- ${"scope1"}
249
- ${"scope2"}
250
- `("should not share cache if id is different", ({scope}: any) => {
251
- // Arrange
252
- const hook1 = clientRenderHook(() => useSharedCache("id1", "scope1"));
253
- const hook2 = clientRenderHook(() => useSharedCache("id2", scope));
254
- hook1.result.current[1]("VALUE_1");
255
-
256
- // Act
257
- hook2.rerender();
258
- const result = hook2.result.current[0];
259
-
260
- // Assert
261
- expect(result).toBeNull();
262
- });
263
- });
@@ -1,297 +0,0 @@
1
- import * as React from "react";
2
- import {useForceUpdate} from "@khanacademy/wonder-blocks-core";
3
- import {DataError, DataErrors} from "../util/data-error";
4
-
5
- import {RequestFulfillment} from "../util/request-fulfillment";
6
- import {Status} from "../util/status";
7
-
8
- import {useSharedCache} from "./use-shared-cache";
9
- import {useRequestInterception} from "./use-request-interception";
10
-
11
- import type {Result, ValidCacheData} from "../util/types";
12
-
13
- import {FetchPolicy} from "../util/types";
14
-
15
- type CachedEffectOptions<TData extends ValidCacheData> = {
16
- /**
17
- * The policy to use when determining how to retrieve the request data from
18
- * cache and network.
19
- *
20
- * Defaults to `FetchPolicy.CacheBeforeNetwork`.
21
- */
22
- fetchPolicy?: typeof FetchPolicy[keyof typeof FetchPolicy];
23
- /**
24
- * When `true`, the effect will not be executed; otherwise, the effect will
25
- * be executed.
26
- *
27
- * If this is set to `true` while the effect is still pending, the pending
28
- * effect will be cancelled.
29
- *
30
- * Default is `false`.
31
- */
32
- skip?: boolean;
33
- /**
34
- * When `true`, the effect will not reset the result to the loading status
35
- * while executing if the requestId changes, instead, returning
36
- * the existing result from before the change; otherwise, the result will
37
- * be set to loading status.
38
- *
39
- * If the status is loading when the changes are made, it will remain as
40
- * loading; old pending effects are discarded on changes and as such this
41
- * value has no effect in that case.
42
- */
43
- retainResultOnChange?: boolean;
44
- /**
45
- * Callback that is invoked if the result for the given hook has changed.
46
- *
47
- * When defined, the hook will invoke this callback whenever it has reason
48
- * to change the result and will not otherwise affect component rendering
49
- * directly.
50
- *
51
- * When not defined, the hook will ensure the component re-renders to pick
52
- * up the latest result.
53
- */
54
- onResultChanged?: (result: Result<TData>) => void;
55
- /**
56
- * Scope to use with the shared cache.
57
- *
58
- * When specified, the given scope will be used to isolate this hook's
59
- * cached results. Otherwise, a shared default scope will be used.
60
- *
61
- * Changing this value after the first call is not supported.
62
- */
63
- scope?: string;
64
- };
65
-
66
- type InflightRequest<TData extends ValidCacheData> = {
67
- requestId: string;
68
- request: Promise<Result<TData>>;
69
- cancel(): void;
70
- };
71
-
72
- const DefaultScope = "useCachedEffect";
73
-
74
- /**
75
- * Hook to execute and cache an async operation on the client.
76
- *
77
- * This hook executes the given handler on the client if there is no
78
- * cached result to use.
79
- *
80
- * Results are cached so they can be shared between equivalent invocations.
81
- * In-flight requests are also shared, so that concurrent calls will
82
- * behave as one might exect. Cache updates invoked by one hook instance
83
- * do not trigger renders in components that use the same requestID; however,
84
- * that should not matter since concurrent requests will share the same
85
- * in-flight request, and subsequent renders will grab from the cache.
86
- *
87
- * Once the request has been tried once and a non-loading response has been
88
- * cached, the request will not executed made again.
89
- */
90
- export const useCachedEffect = <TData extends ValidCacheData>(
91
- requestId: string,
92
- handler: () => Promise<TData>,
93
- options: CachedEffectOptions<TData> = {} as Partial<
94
- CachedEffectOptions<TData>
95
- >,
96
- ): [Result<TData>, () => void] => {
97
- const {
98
- fetchPolicy = FetchPolicy.CacheBeforeNetwork,
99
- skip: hardSkip = false,
100
- retainResultOnChange = false,
101
- onResultChanged,
102
- scope = DefaultScope,
103
- } = options;
104
-
105
- // Plug in to the request interception framework for code that wants
106
- // to use that.
107
- const interceptedHandler = useRequestInterception(requestId, handler);
108
-
109
- // Instead of using state, which would be local to just this hook instance,
110
- // we use a shared in-memory cache.
111
- const [mostRecentResult, setMostRecentResult] = useSharedCache<
112
- Result<TData>
113
- >( // The key of the cached item
114
- requestId, // The scope of the cached items
115
- // No default value. We don't want the loading status there; to ensure
116
- // that all calls when the request is in-flight will update once that
117
- // request is done, we want the cache to be empty until that point.
118
- scope,
119
- );
120
- const forceUpdate = useForceUpdate();
121
- // For the NetworkOnly fetch policy, we ignore the cached value.
122
- // So we need somewhere else to store the network value.
123
- const networkResultRef = React.useRef<Result<TData> | null>();
124
-
125
- // Set up the function that will do the fetching.
126
- const currentRequestRef = React.useRef<InflightRequest<TData> | null>();
127
- const fetchRequest = React.useMemo(() => {
128
- // We aren't using useCallback here because we need to make sure that
129
- // if we are rememo-izing, we cancel any inflight request for the old
130
- // callback.
131
- currentRequestRef.current?.cancel();
132
- currentRequestRef.current = null;
133
- networkResultRef.current = null;
134
-
135
- const fetchFn = () => {
136
- if (fetchPolicy === FetchPolicy.CacheOnly) {
137
- throw new DataError(
138
- "Cannot fetch with CacheOnly policy",
139
- DataErrors.NotAllowed,
140
- );
141
- }
142
- // We use our request fulfillment here so that in-flight
143
- // requests are shared. In order to ensure that we don't share
144
- // in-flight requests for different scopes, we add the scope to the
145
- // requestId.
146
- // We do this as a courtesy to simplify usage in sandboxed
147
- // uses like storybook where we want each story to perform their
148
- // own requests from scratch and not share inflight requests across
149
- // stories.
150
- // Since this only occurs here, nothing else will care about this
151
- // change except the request tracking.
152
- const request = RequestFulfillment.Default.fulfill(
153
- `${requestId}|${scope}`,
154
- {
155
- handler: interceptedHandler,
156
- },
157
- );
158
-
159
- if (request === currentRequestRef.current?.request) {
160
- // The request inflight is the same, so do nothing.
161
- // NOTE: Perhaps if invoked via a refetch, we will want to
162
- // override this behavior and force a new request?
163
- return;
164
- }
165
-
166
- // Clear the last network result.
167
- networkResultRef.current = null;
168
-
169
- // Cancel the previous request.
170
- currentRequestRef.current?.cancel();
171
-
172
- // TODO(somewhatabstract, FEI-4276):
173
- // Until our RequestFulfillment API supports cancelling/aborting, we
174
- // will have to do it.
175
- let cancel = false;
176
-
177
- // NOTE: Our request fulfillment handles the error cases here.
178
- // Catching shouldn't serve a purpose.
179
- // eslint-disable-next-line promise/catch-or-return
180
- request.then((result) => {
181
- currentRequestRef.current = null;
182
- if (cancel) {
183
- // We don't modify our result if the request was cancelled
184
- // as it means that this hook no longer cares about that old
185
- // request.
186
- return;
187
- }
188
-
189
- // Now we need to update the cache and notify or force a rerender.
190
- setMostRecentResult(result);
191
- networkResultRef.current = result;
192
-
193
- if (onResultChanged != null) {
194
- // If we have a callback, call it to let our caller know we
195
- // got a result.
196
- onResultChanged(result);
197
- } else {
198
- // If there's no callback, and this is using cache in some
199
- // capacity, just force a rerender.
200
- forceUpdate();
201
- }
202
- return; // Shut up eslint always-return rule.
203
- });
204
-
205
- currentRequestRef.current = {
206
- requestId,
207
- request,
208
- cancel() {
209
- cancel = true;
210
- RequestFulfillment.Default.abort(requestId);
211
- },
212
- };
213
- };
214
-
215
- // Now we can return the new fetch function.
216
- return fetchFn;
217
-
218
- // We deliberately ignore the handler here because we want folks to use
219
- // interceptor functions inline in props for simplicity. This is OK
220
- // since changing the handler without changing the requestId doesn't
221
- // really make sense - the same requestId should be handled the same as
222
- // each other.
223
- // eslint-disable-next-line react-hooks/exhaustive-deps
224
- }, [
225
- requestId,
226
- onResultChanged,
227
- forceUpdate,
228
- setMostRecentResult,
229
- fetchPolicy,
230
- ]);
231
-
232
- // Calculate if we want to fetch the result or not.
233
- // If this is true, we will do a new fetch, cancelling the previous fetch
234
- // if there is one inflight.
235
- const shouldFetch = React.useMemo(() => {
236
- if (hardSkip) {
237
- // We don't fetch if we've been told to hard skip.
238
- return false;
239
- }
240
-
241
- switch (fetchPolicy) {
242
- case FetchPolicy.CacheOnly:
243
- // Don't want to do a network request if we're only
244
- // interested in the cache.
245
- return false;
246
-
247
- case FetchPolicy.CacheBeforeNetwork:
248
- // If we don't have a cached value then we need to fetch.
249
- return mostRecentResult == null;
250
-
251
- case FetchPolicy.CacheAndNetwork:
252
- case FetchPolicy.NetworkOnly:
253
- // We don't care about the cache. If we don't have a network
254
- // result, then we need to fetch one.
255
- return networkResultRef.current == null;
256
- }
257
- }, [mostRecentResult, fetchPolicy, hardSkip]);
258
-
259
- React.useEffect(() => {
260
- if (!shouldFetch) {
261
- return;
262
- }
263
- fetchRequest();
264
- return () => {
265
- currentRequestRef.current?.cancel();
266
- currentRequestRef.current = null;
267
- };
268
- }, [shouldFetch, fetchRequest]);
269
-
270
- // We track the last result we returned in order to support the
271
- // "retainResultOnChange" option. To begin, the last result is no-data.
272
- const lastResultAgnosticOfIdRef = React.useRef<Result<TData>>(
273
- Status.noData<TData>(),
274
- );
275
- // The default return value is:
276
- // - The last result we returned if we're retaining results on change.
277
- // - The no-data state if shouldFetch is false, and therefore there is no
278
- // in-flight request.
279
- // - Otherwise, the loading state (we can assume there's an inflight
280
- // request if skip is not true).
281
- const loadingResult = retainResultOnChange
282
- ? lastResultAgnosticOfIdRef.current
283
- : shouldFetch
284
- ? Status.loading<TData>()
285
- : Status.noData<TData>();
286
-
287
- // Loading and no-data are transient states, so we only use them here;
288
- // they're not something we cache.
289
- const result: Result<TData> =
290
- (fetchPolicy === FetchPolicy.NetworkOnly
291
- ? networkResultRef.current
292
- : mostRecentResult) ?? loadingResult;
293
- lastResultAgnosticOfIdRef.current = result;
294
-
295
- // We return the result and a function for triggering a refetch.
296
- return [result, fetchRequest];
297
- };
@@ -1,49 +0,0 @@
1
- import {useContext, useRef, useMemo} from "react";
2
-
3
- import {mergeGqlContext} from "../util/merge-gql-context";
4
- import {GqlRouterContext} from "../util/gql-router-context";
5
- import {GqlError, GqlErrors} from "../util/gql-error";
6
-
7
- import type {GqlRouterConfiguration, GqlContext} from "../util/gql-types";
8
-
9
- /**
10
- * Construct a GqlRouterContext from the current one and partial context.
11
- */
12
- export const useGqlRouterContext = <TContext extends GqlContext>(
13
- contextOverrides: Partial<TContext> = {} as Partial<TContext>,
14
- ): GqlRouterConfiguration<TContext> => {
15
- // This hook only works if the `GqlRouter` has been used to setup context.
16
- const gqlRouterContext = useContext(GqlRouterContext);
17
- if (gqlRouterContext == null) {
18
- throw new GqlError("No GqlRouter", GqlErrors.Internal);
19
- }
20
-
21
- const {fetch, defaultContext} = gqlRouterContext;
22
- const contextRef = useRef<TContext>(defaultContext);
23
- const mergedContext = mergeGqlContext(defaultContext, contextOverrides);
24
-
25
- // Now, we can see if this represents a new context and if so,
26
- // update our ref and return the merged value.
27
- const refKeys = Object.keys(contextRef.current);
28
- const mergedKeys = Object.keys(mergedContext);
29
- const shouldWeUpdateRef =
30
- refKeys.length !== mergedKeys.length ||
31
- mergedKeys.every(
32
- (key) => contextRef.current[key] !== mergedContext[key],
33
- );
34
- if (shouldWeUpdateRef) {
35
- contextRef.current = mergedContext;
36
- }
37
-
38
- // OK, now we're up-to-date, let's memoize our final result.
39
- const finalContext = contextRef.current;
40
- const finalRouterContext = useMemo(
41
- () => ({
42
- fetch,
43
- defaultContext: finalContext,
44
- }),
45
- [fetch, finalContext],
46
- );
47
-
48
- return finalRouterContext;
49
- };
@@ -1,58 +0,0 @@
1
- import {useCallback} from "react";
2
-
3
- import {mergeGqlContext} from "../util/merge-gql-context";
4
- import {useGqlRouterContext} from "./use-gql-router-context";
5
- import {getGqlDataFromResponse} from "../util/get-gql-data-from-response";
6
-
7
- import type {
8
- GqlContext,
9
- GqlOperation,
10
- GqlFetchOptions,
11
- } from "../util/gql-types";
12
-
13
- interface GqlFetchFn<TContext extends GqlContext> {
14
- <TData, TVariables extends Record<any, any>>(
15
- operation: GqlOperation<TData, TVariables>,
16
- options?: GqlFetchOptions<TVariables, TContext>,
17
- ): Promise<TData>;
18
- }
19
-
20
- /**
21
- * Hook to obtain a gqlFetch function for performing GraphQL requests.
22
- *
23
- * The fetch function will resolve null if the request was aborted, otherwise
24
- * it will resolve the data returned by the GraphQL server.
25
- *
26
- * Context is merged with the default context provided to the GqlRouter.
27
- * Values in the partial context given to the returned fetch function will
28
- * only be included if they have a value other than undefined.
29
- */
30
- export const useGql = <TContext extends GqlContext>(
31
- context: Partial<TContext> = {} as Partial<TContext>,
32
- ): GqlFetchFn<TContext> => {
33
- // This hook only works if the `GqlRouter` has been used to setup context.
34
- const gqlRouterContext = useGqlRouterContext(context);
35
-
36
- // Let's memoize the gqlFetch function we create based off our context.
37
- // That way, even if the context happens to change, if its values don't
38
- // we give the same function instance back to our callers instead of
39
- // making a new one. That then means they can safely use the return value
40
- // in hooks deps without fear of it triggering extra renders.
41
- const gqlFetch: GqlFetchFn<TContext> = useCallback(
42
- <TData, TVariables extends Record<any, any>>(
43
- operation: GqlOperation<TData, TVariables>,
44
- options: GqlFetchOptions<TVariables, TContext> = Object.freeze({}),
45
- ): Promise<TData> => {
46
- const {fetch, defaultContext} = gqlRouterContext;
47
- const {variables, context = {}} = options;
48
- const finalContext = mergeGqlContext(defaultContext, context);
49
-
50
- // Invoke the fetch and extract the data.
51
- return fetch(operation, variables, finalContext).then((response) =>
52
- getGqlDataFromResponse<TData>(response),
53
- );
54
- },
55
- [gqlRouterContext],
56
- );
57
- return gqlFetch;
58
- };