@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,68 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {resultFromCacheEntry} from "../result-from-cache-entry.js";
|
|
3
|
+
|
|
4
|
+
describe("#resultFromCacheEntry", () => {
|
|
5
|
+
it("should return loading status if cache entry is null", () => {
|
|
6
|
+
// Arrange
|
|
7
|
+
const cacheEntry = null;
|
|
8
|
+
|
|
9
|
+
// Act
|
|
10
|
+
const result = resultFromCacheEntry(cacheEntry);
|
|
11
|
+
|
|
12
|
+
// Assert
|
|
13
|
+
expect(result).toStrictEqual({
|
|
14
|
+
status: "loading",
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should return success status if cache entry has data", () => {
|
|
19
|
+
// Arrange
|
|
20
|
+
const cacheEntry = {
|
|
21
|
+
data: "DATA",
|
|
22
|
+
error: null,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Act
|
|
26
|
+
const result = resultFromCacheEntry(cacheEntry);
|
|
27
|
+
|
|
28
|
+
// Assert
|
|
29
|
+
expect(result).toStrictEqual({
|
|
30
|
+
status: "success",
|
|
31
|
+
data: "DATA",
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should return error status if cache entry has no data and no error", () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const cacheEntry: any = {
|
|
38
|
+
data: null,
|
|
39
|
+
error: null,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Act
|
|
43
|
+
const result = resultFromCacheEntry(cacheEntry);
|
|
44
|
+
|
|
45
|
+
// Assert
|
|
46
|
+
expect(result).toStrictEqual({
|
|
47
|
+
status: "error",
|
|
48
|
+
error: "Loaded result has invalid state where data and error are missing",
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should return error status if cache entry has error", () => {
|
|
53
|
+
// Arrange
|
|
54
|
+
const cacheEntry: any = {
|
|
55
|
+
data: null,
|
|
56
|
+
error: "ERROR",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Act
|
|
60
|
+
const result = resultFromCacheEntry(cacheEntry);
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
expect(result).toStrictEqual({
|
|
64
|
+
status: "error",
|
|
65
|
+
error: "ERROR",
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {GqlError, GqlErrors} from "./gql-error.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validate a GQL operation response and extract the data.
|
|
6
|
+
*/
|
|
7
|
+
export const getGqlDataFromResponse = async <TData>(
|
|
8
|
+
response: Response,
|
|
9
|
+
): Promise<TData> => {
|
|
10
|
+
// Get the response as text, that way we can use the text in error
|
|
11
|
+
// messaging, should our parsing fail.
|
|
12
|
+
const bodyText = await response.text();
|
|
13
|
+
let result;
|
|
14
|
+
try {
|
|
15
|
+
result = JSON.parse(bodyText);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
throw new GqlError("Failed to parse response", GqlErrors.Parse, {
|
|
18
|
+
metadata: {
|
|
19
|
+
statusCode: response.status,
|
|
20
|
+
bodyText,
|
|
21
|
+
},
|
|
22
|
+
cause: e,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check for a bad status code.
|
|
27
|
+
if (response.status >= 300) {
|
|
28
|
+
throw new GqlError("Response unsuccessful", GqlErrors.Network, {
|
|
29
|
+
metadata: {
|
|
30
|
+
statusCode: response.status,
|
|
31
|
+
result,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check that we have a valid result payload.
|
|
37
|
+
if (
|
|
38
|
+
// Flow shouldn't be warning about this.
|
|
39
|
+
// $FlowIgnore[method-unbinding]
|
|
40
|
+
!Object.prototype.hasOwnProperty.call(result, "data") &&
|
|
41
|
+
// Flow shouldn't be warning about this.
|
|
42
|
+
// $FlowIgnore[method-unbinding]
|
|
43
|
+
!Object.prototype.hasOwnProperty.call(result, "errors")
|
|
44
|
+
) {
|
|
45
|
+
throw new GqlError("Server response missing", GqlErrors.BadResponse, {
|
|
46
|
+
metadata: {
|
|
47
|
+
statusCode: response.status,
|
|
48
|
+
result,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If the response payload has errors, throw an error.
|
|
54
|
+
if (
|
|
55
|
+
result.errors != null &&
|
|
56
|
+
Array.isArray(result.errors) &&
|
|
57
|
+
result.errors.length > 0
|
|
58
|
+
) {
|
|
59
|
+
throw new GqlError("GraphQL errors", GqlErrors.ErrorResult, {
|
|
60
|
+
metadata: {
|
|
61
|
+
statusCode: response.status,
|
|
62
|
+
result,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// We got here, so return the data.
|
|
68
|
+
return result.data;
|
|
69
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {KindError, Errors} from "@khanacademy/wonder-stuff-core";
|
|
3
|
+
import type {Metadata} from "@khanacademy/wonder-stuff-core";
|
|
4
|
+
|
|
5
|
+
type GqlErrorOptions = {|
|
|
6
|
+
metadata?: ?Metadata,
|
|
7
|
+
cause?: ?Error,
|
|
8
|
+
|};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Error kinds for GqlError.
|
|
12
|
+
*/
|
|
13
|
+
export const GqlErrors = Object.freeze({
|
|
14
|
+
...Errors,
|
|
15
|
+
Network: "Network",
|
|
16
|
+
Parse: "Parse",
|
|
17
|
+
BadResponse: "BadResponse",
|
|
18
|
+
ErrorResult: "ErrorResult",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* An error from the GQL API.
|
|
23
|
+
*/
|
|
24
|
+
export class GqlError extends KindError {
|
|
25
|
+
constructor(
|
|
26
|
+
message: string,
|
|
27
|
+
kind: $Values<typeof GqlErrors>,
|
|
28
|
+
{metadata, cause}: GqlErrorOptions = ({}: $Shape<GqlErrorOptions>),
|
|
29
|
+
) {
|
|
30
|
+
super(message, kind, {
|
|
31
|
+
metadata,
|
|
32
|
+
cause,
|
|
33
|
+
prefix: "Gql",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* Operation types.
|
|
4
|
+
*/
|
|
5
|
+
export type GqlOperationType = "mutation" | "query";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A GraphQL operation.
|
|
9
|
+
*/
|
|
10
|
+
export type GqlOperation<
|
|
11
|
+
TType: GqlOperationType,
|
|
12
|
+
// TData is not used to define a field on this type, but it is used
|
|
13
|
+
// to ensure that calls using this operation will properly return the
|
|
14
|
+
// correct data type.
|
|
15
|
+
// eslint-disable-next-line no-unused-vars
|
|
16
|
+
TData,
|
|
17
|
+
// TVariables is not used to define a field on this type, but it is used
|
|
18
|
+
// to ensure that calls using this operation will properly consume the
|
|
19
|
+
// correct variables type.
|
|
20
|
+
// eslint-disable-next-line no-unused-vars
|
|
21
|
+
TVariables: {...} = Empty,
|
|
22
|
+
> = {
|
|
23
|
+
type: TType,
|
|
24
|
+
id: string,
|
|
25
|
+
// We allow other things here to be passed along to the fetch function.
|
|
26
|
+
// For example, we might want to pass the full query/mutation definition
|
|
27
|
+
// as a string here to allow that to be sent to an Apollo server that
|
|
28
|
+
// expects it. This is a courtesy to calling code; these additional
|
|
29
|
+
// values are ignored by WB Data, and passed through as-is.
|
|
30
|
+
...
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type GqlContext = {|
|
|
34
|
+
[key: string]: string,
|
|
35
|
+
|};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Functions that make fetches of GQL operations.
|
|
39
|
+
*/
|
|
40
|
+
export type FetchFn<TType, TData, TVariables: {...}, TContext: GqlContext> = (
|
|
41
|
+
operation: GqlOperation<TType, TData, TVariables>,
|
|
42
|
+
variables: ?TVariables,
|
|
43
|
+
context: TContext,
|
|
44
|
+
) => Promise<Response>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The configuration stored in the GqlRouterContext context.
|
|
48
|
+
*/
|
|
49
|
+
export type GqlRouterConfiguration<TContext: GqlContext> = {|
|
|
50
|
+
fetch: FetchFn<any, any, any, any>,
|
|
51
|
+
defaultContext: TContext,
|
|
52
|
+
|};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Options for configuring a GQL fetch.
|
|
56
|
+
*/
|
|
57
|
+
export type GqlFetchOptions<TVariables: {...}, TContext: GqlContext> = {|
|
|
58
|
+
variables?: TVariables,
|
|
59
|
+
context?: Partial<TContext>,
|
|
60
|
+
|};
|
package/src/util/memory-cache.js
CHANGED
|
@@ -25,12 +25,11 @@ function deepClone<T: {...}>(source: T | $ReadOnly<T>): $ReadOnly<T> {
|
|
|
25
25
|
*
|
|
26
26
|
* Special case cache implementation for the memory cache.
|
|
27
27
|
*
|
|
28
|
-
* This is only used within our framework
|
|
29
|
-
* provide this as a custom cache as the framework will default to this in the
|
|
30
|
-
* absence of a custom cache. We use this for SSR too (see ./response-cache.js).
|
|
28
|
+
* This is only used within our framework for SSR (see ./response-cache.js).
|
|
31
29
|
*/
|
|
32
30
|
export default class MemoryCache<TOptions, TData: ValidData>
|
|
33
|
-
implements ICache<TOptions, TData>
|
|
31
|
+
implements ICache<TOptions, TData>
|
|
32
|
+
{
|
|
34
33
|
_cache: Cache;
|
|
35
34
|
|
|
36
35
|
constructor(source: ?$ReadOnly<Cache> = null) {
|
|
@@ -52,6 +51,11 @@ export default class MemoryCache<TOptions, TData: ValidData>
|
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Indicate if this cache is being used or now.
|
|
56
|
+
*
|
|
57
|
+
* When the cache has entries, returns `true`; otherwise, returns `false`.
|
|
58
|
+
*/
|
|
55
59
|
get inUse(): boolean {
|
|
56
60
|
return Object.keys(this._cache).length > 0;
|
|
57
61
|
}
|
|
@@ -67,9 +71,7 @@ export default class MemoryCache<TOptions, TData: ValidData>
|
|
|
67
71
|
): void => {
|
|
68
72
|
const requestType = handler.type;
|
|
69
73
|
|
|
70
|
-
const frozenEntry = Object.
|
|
71
|
-
? entry
|
|
72
|
-
: Object.freeze(entry);
|
|
74
|
+
const frozenEntry = Object.freeze(entry);
|
|
73
75
|
|
|
74
76
|
// Ensure we have a cache location for this handler type.
|
|
75
77
|
this._cache[requestType] = this._cache[requestType] || {};
|
|
@@ -156,16 +158,19 @@ export default class MemoryCache<TOptions, TData: ValidData>
|
|
|
156
158
|
return 0;
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
// Apply the predicate to what we have cached.
|
|
160
161
|
let removedCount = 0;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
predicate(key, (entry: any))
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
if (typeof predicate === "function") {
|
|
163
|
+
// Apply the predicate to what we have cached.
|
|
164
|
+
for (const [key, entry] of Object.entries(handlerCache)) {
|
|
165
|
+
if (predicate(key, (entry: any))) {
|
|
166
|
+
removedCount++;
|
|
167
|
+
delete handlerCache[key];
|
|
168
|
+
}
|
|
168
169
|
}
|
|
170
|
+
} else {
|
|
171
|
+
// We're removing everything so delete the entire subcache.
|
|
172
|
+
removedCount = Object.keys(handlerCache).length;
|
|
173
|
+
delete this._cache[requestType];
|
|
169
174
|
}
|
|
170
175
|
return removedCount;
|
|
171
176
|
};
|
|
@@ -76,6 +76,8 @@ export class RequestFulfillment {
|
|
|
76
76
|
delete handlerRequests[key];
|
|
77
77
|
/**
|
|
78
78
|
* Let's cache the data!
|
|
79
|
+
*
|
|
80
|
+
* NOTE: This only caches when we're server side.
|
|
79
81
|
*/
|
|
80
82
|
return cacheData<TOptions, TData>(handler, options, data);
|
|
81
83
|
})
|
|
@@ -83,6 +85,8 @@ export class RequestFulfillment {
|
|
|
83
85
|
delete handlerRequests[key];
|
|
84
86
|
/**
|
|
85
87
|
* Let's cache the error!
|
|
88
|
+
*
|
|
89
|
+
* NOTE: This only caches when we're server side.
|
|
86
90
|
*/
|
|
87
91
|
return cacheError<TOptions, TData>(handler, options, error);
|
|
88
92
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
import type {ValidData,
|
|
2
|
+
import type {ValidData, IRequestHandler} from "./types.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Base implementation for creating a request handler.
|
|
@@ -8,18 +8,13 @@ import type {ValidData, CacheEntry, IRequestHandler, ICache} from "./types.js";
|
|
|
8
8
|
* use with the Wonder Blocks Data framework.
|
|
9
9
|
*/
|
|
10
10
|
export default class RequestHandler<TOptions, TData: ValidData>
|
|
11
|
-
implements IRequestHandler<TOptions, TData>
|
|
11
|
+
implements IRequestHandler<TOptions, TData>
|
|
12
|
+
{
|
|
12
13
|
_type: string;
|
|
13
|
-
_cache: ?ICache<TOptions, TData>;
|
|
14
14
|
_hydrate: boolean;
|
|
15
15
|
|
|
16
|
-
constructor(
|
|
17
|
-
type: string,
|
|
18
|
-
cache?: ICache<TOptions, TData>,
|
|
19
|
-
hydrate?: boolean = true,
|
|
20
|
-
) {
|
|
16
|
+
constructor(type: string, hydrate?: boolean = true) {
|
|
21
17
|
this._type = type;
|
|
22
|
-
this._cache = cache || null;
|
|
23
18
|
this._hydrate = !!hydrate;
|
|
24
19
|
}
|
|
25
20
|
|
|
@@ -27,29 +22,10 @@ export default class RequestHandler<TOptions, TData: ValidData>
|
|
|
27
22
|
return this._type;
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
get cache(): ?ICache<TOptions, TData> {
|
|
31
|
-
return this._cache;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
25
|
get hydrate(): boolean {
|
|
35
26
|
return this._hydrate;
|
|
36
27
|
}
|
|
37
28
|
|
|
38
|
-
shouldRefreshCache(
|
|
39
|
-
options: TOptions,
|
|
40
|
-
cachedEntry: ?$ReadOnly<CacheEntry<TData>>,
|
|
41
|
-
): boolean {
|
|
42
|
-
/**
|
|
43
|
-
* By default, the cache needs a refresh if the current entry is an
|
|
44
|
-
* error.
|
|
45
|
-
*
|
|
46
|
-
* This means that an error will cause a re-request on render.
|
|
47
|
-
* Useful if the server rendered an error, as it means the client
|
|
48
|
-
* will update after rehydration.
|
|
49
|
-
*/
|
|
50
|
-
return cachedEntry == null || cachedEntry.error != null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
29
|
getKey(options: TOptions): string {
|
|
54
30
|
try {
|
|
55
31
|
return options === undefined
|
|
@@ -14,12 +14,6 @@ interface IRequestHandler<TOptions, TData> {
|
|
|
14
14
|
*/
|
|
15
15
|
get type(): string;
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* A custom cache to use with data that this handler requests.
|
|
19
|
-
* This only affects client-side caching of data.
|
|
20
|
-
*/
|
|
21
|
-
get cache(): ?ICache<TOptions, TData>;
|
|
22
|
-
|
|
23
17
|
/**
|
|
24
18
|
* When true, server-side results are cached and hydrated in the client.
|
|
25
19
|
* When false, the server-side cache is not used and results are not
|
|
@@ -29,17 +23,6 @@ interface IRequestHandler<TOptions, TData> {
|
|
|
29
23
|
*/
|
|
30
24
|
get hydrate(): boolean;
|
|
31
25
|
|
|
32
|
-
/**
|
|
33
|
-
* Determine if the cached data should be refreshed.
|
|
34
|
-
*
|
|
35
|
-
* If this returns true, the framework will use the currently cached value
|
|
36
|
-
* but also request a new value.
|
|
37
|
-
*/
|
|
38
|
-
shouldRefreshCache(
|
|
39
|
-
options: TOptions,
|
|
40
|
-
cachedEntry: ?$ReadOnly<CacheEntry<TData>>,
|
|
41
|
-
): boolean;
|
|
42
|
-
|
|
43
26
|
/**
|
|
44
27
|
* Get the key to use for a given request. This should be idempotent for a
|
|
45
28
|
* given options set if you want caching to work across requests.
|
|
@@ -52,13 +35,6 @@ The constructor requires a `type` to identify your handler. This should be uniqu
|
|
|
52
35
|
among the handlers that are used across your application, otherwise, requests
|
|
53
36
|
may be fulfilled by the wrong handler.
|
|
54
37
|
|
|
55
|
-
There is also an optional constructor argument, `cache`, which can be used to
|
|
56
|
-
provide a custom cache for use with data the handler fulfills. Custom caches
|
|
57
|
-
must implement the `ICache<TOptions, TData>` interface. If this is omitted, the
|
|
58
|
-
core Wonder Blocks Data in-memory cache will be used. If you want to avoid
|
|
59
|
-
caching in memory, see `NoCache`, which is a caching strategy that eliminates
|
|
60
|
-
the use of caching entirely.
|
|
61
|
-
|
|
62
38
|
The `fulfillRequest` method of this class is not implemented and will throw if
|
|
63
39
|
called. Subclasses will need to implement this method.
|
|
64
40
|
|
|
@@ -73,11 +49,3 @@ SSR process is tracking the data for hydration. An example of setting this to
|
|
|
73
49
|
false might be when you are using Apollo Client. In that scenario, you may use
|
|
74
50
|
Apollo Cache to store and hydrate the data, while using Wonder Blocks Data to
|
|
75
51
|
track and fulfill any query requests made via Apollo Client.
|
|
76
|
-
|
|
77
|
-
Finally, the `shouldRefreshCache` method is provided for cases where a handler
|
|
78
|
-
may want control over cache freshness. By default, this will return `true` for
|
|
79
|
-
error results or a missing value. However, in some cases, handlers may want to
|
|
80
|
-
make sure cached entries are not stale, and so may return `true` from the
|
|
81
|
-
`shouldRefreshCache` method to instruct the framework to make a new request.
|
|
82
|
-
The existing cached value will still be used, but an updated value will be
|
|
83
|
-
requested.
|
|
@@ -25,9 +25,8 @@ type RequestCache = {
|
|
|
25
25
|
*
|
|
26
26
|
* INTERNAL USE ONLY
|
|
27
27
|
*/
|
|
28
|
-
export const TrackerContext: React.Context<?TrackerFn> =
|
|
29
|
-
null
|
|
30
|
-
);
|
|
28
|
+
export const TrackerContext: React.Context<?TrackerFn> =
|
|
29
|
+
new React.createContext<?TrackerFn>(null);
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
32
|
* The default instance is stored here.
|