[Blocks] Initial implementation of cache and data/fetch (#18774)
* Rename ReactCache -> ReactCacheOld We still use it in some tests so I'm going to leave it for now. I'll start making the new one in parallel in the react package. * Add react/unstable-cache entry point * Add react-data entry point * Initial implementation of cache and data/fetch * Address review
Dan Abramov committed
Apr 29, 2020 at 19:14 UTC
515326753b15eb247493b1b5c657eee1bc515337
25 files changed
+367
-98
fixtures/blocks/src/App.js
+2
-1
@@ -7,9 +7,10 @@
7
8
import React, {useReducer, useTransition, Suspense} from 'react';
9
import loadPost from './Post';
10
-import {createCache, CacheProvider} from './lib/cache';
10
+import {createCache, CacheProvider} from 'react/unstable-cache';
11
12
const initialState = {
13
+ // TODO: use this for invalidation.
14
cache: createCache(),
15
params: {id: 1},
16
RootBlock: loadPost({id: 1}),
fixtures/blocks/src/Comments.js
+4
-2
@@ -6,11 +6,13 @@
6
*/
7
8
import * as React from 'react';
9
-import {fetch} from './lib/data';
9
+import {fetch} from 'react-data/fetch';
10
11
function load(postId) {
12
return {
13
- comments: fetch('http://localhost:3001/comments?postId=' + postId),
13
+ comments: JSON.parse(
14
+ fetch('http://localhost:3001/comments?postId=' + postId)
15
+ ),
16
};
17
}
18
fixtures/blocks/src/Post.js
+2
-2
@@ -7,12 +7,12 @@
7
8
import * as React from 'react';
9
import {block, Suspense} from 'react';
10
-import {fetch} from './lib/data';
10
+import {fetch} from 'react-data/fetch';
11
import loadComments from './Comments';
12
13
function load(params) {
14
return {
15
- post: fetch('http://localhost:3001/posts/' + params.id),
15
+ post: JSON.parse(fetch('http://localhost:3001/posts/' + params.id)),
16
Comments: loadComments(params.id),
17
};
18
}
fixtures/blocks/src/lib/cache.js
deleted
-30
@@ -1,30 +0,0 @@
1
-/**
2
- * Copyright (c) Facebook, Inc. and its affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- */
7
-
8
-import {createContext} from 'react';
9
-
10
-// TODO: clean up and move to react/cache.
11
-
12
-// TODO: cancellation token.
13
-
14
-// TODO: does there need to be default context?
15
-
16
-const CacheContext = createContext(null);
17
-
18
-export const CacheProvider = CacheContext.Provider;
19
-
20
-// TODO: use this for invalidation.
21
-
22
-export function createCache() {
23
- return new Map();
24
-}
25
-
26
-export function readCache() {
27
- // TODO: this doesn't subscribe.
28
- // But we really want load context anyway.
29
- return CacheContext._currentValue;
30
-}
fixtures/blocks/src/lib/data.js
deleted
-59
@@ -1,59 +0,0 @@
1
-/**
2
- * Copyright (c) Facebook, Inc. and its affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- */
7
-
8
-import {readCache} from './cache';
9
-
10
-// TODO: clean up and move to react-data/fetch.
11
-
12
-// TODO: some other data provider besides fetch.
13
-
14
-// TODO: base agnostic helper like createResource. Maybe separate.
15
-
16
-let sigil = {};
17
-
18
-function readFetchMap() {
19
- const cache = readCache();
20
- if (!cache.has(sigil)) {
21
- cache.set(sigil, new Map());
22
- }
23
- return cache.get(sigil);
24
-}
25
-
26
-export function fetch(url) {
27
- const map = readFetchMap();
28
- let entry = map.get(url);
29
- if (entry === undefined) {
30
- entry = {
31
- status: 'pending',
32
- result: new Promise(resolve => {
33
- let xhr = new XMLHttpRequest();
34
- xhr.onload = function() {
35
- entry.result = JSON.parse(xhr.response);
36
- entry.status = 'resolved';
37
- resolve();
38
- };
39
- xhr.onerror = function(err) {
40
- entry.result = err;
41
- entry.status = 'rejected';
42
- resolve();
43
- };
44
- xhr.open('GET', url);
45
- xhr.send();
46
- }),
47
- };
48
- map.set(url, entry);
49
- }
50
- switch (entry.status) {
51
- case 'resolved':
52
- return entry.result;
53
- case 'pending':
54
- case 'rejected':
55
- throw entry.result;
56
- default:
57
- throw new Error();
58
- }
59
-}
packages/react-cache/index.js
+1
-1
@@ -9,4 +9,4 @@
9
10
'use strict';
11
12
-export * from './src/ReactCache';
12
+export * from './src/ReactCacheOld';
packages/react-cache/src/ReactCacheOld.js
renamed
packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js
renamed
packages/react-data/README.md
new
+12
@@ -0,0 +1,12 @@
1
+# react-data
2
+
3
+This package is meant to be used alongside yet-to-be-released, experimental React features. It's unlikely to be useful in any other context.
4
+
5
+**Do not use in a real application.** We're publishing this early for
6
+demonstration purposes.
7
+
8
+**Use it at your own risk.**
9
+
10
+# No, Really, It Is Unstable
11
+
12
+The API ~~may~~ will change wildly between versions.
packages/react-data/fetch.js
new
+12
@@ -0,0 +1,12 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+'use strict';
11
+
12
+export * from './src/fetch/ReactDataFetch';
packages/react-data/index.js
new
+12
@@ -0,0 +1,12 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+'use strict';
11
+
12
+export * from './src/ReactData';
packages/react-data/npm/fetch.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-data-fetch.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-data-fetch.development.js');
7
+}
packages/react-data/npm/index.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-data.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-data.development.js');
7
+}
packages/react-data/package.json
new
+22
@@ -0,0 +1,22 @@
1
+{
2
+ "private": true,
3
+ "name": "react-data",
4
+ "description": "Helpers for creating React data sources",
5
+ "version": "0.0.0",
6
+ "repository": {
7
+ "type" : "git",
8
+ "url" : "https://github.com/facebook/react.git",
9
+ "directory": "packages/react-data"
10
+ },
11
+ "files": [
12
+ "LICENSE",
13
+ "README.md",
14
+ "build-info.json",
15
+ "index.js",
16
+ "fetch.js",
17
+ "cjs/"
18
+ ],
19
+ "peerDependencies": {
20
+ "react": "^16.13.1"
21
+ }
22
+}
packages/react-data/src/ReactData.js
new
+12
@@ -0,0 +1,12 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export function createResource(): any {
11
+ // TODO
12
+}
packages/react-data/src/__tests__/ReactData-test.js
new
+23
@@ -0,0 +1,23 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+describe('ReactData', () => {
13
+ let ReactData;
14
+
15
+ beforeEach(() => {
16
+ ReactData = require('react-data');
17
+ });
18
+
19
+ // TODO: test something useful.
20
+ it('exports something', () => {
21
+ expect(ReactData.createResource).not.toBe(undefined);
22
+ });
23
+});
packages/react-data/src/__tests__/ReactDataFetch-test.js
new
+23
@@ -0,0 +1,23 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+describe('ReactDataFetch', () => {
13
+ let ReactDataFetch;
14
+
15
+ beforeEach(() => {
16
+ ReactDataFetch = require('react-data/fetch');
17
+ });
18
+
19
+ // TODO: test something useful.
20
+ it('exports something', () => {
21
+ expect(ReactDataFetch.fetch).not.toBe(undefined);
22
+ });
23
+});
packages/react-data/src/fetch/ReactDataFetch.js
new
+93
@@ -0,0 +1,93 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {Wakeable} from 'shared/ReactTypes';
11
+
12
+import {readCache} from 'react/unstable-cache';
13
+
14
+const Pending = 0;
15
+const Resolved = 1;
16
+const Rejected = 2;
17
+
18
+type PendingResult = {|
19
+ status: 0,
20
+ value: Wakeable,
21
+|};
22
+
23
+type ResolvedResult = {|
24
+ status: 1,
25
+ value: mixed,
26
+|};
27
+
28
+type RejectedResult = {|
29
+ status: 2,
30
+ value: mixed,
31
+|};
32
+
33
+type Result = PendingResult | ResolvedResult | RejectedResult;
34
+
35
+const fetchKey = {};
36
+
37
+function readResultMap(): Map<string, Result> {
38
+ const resources = readCache().resources;
39
+ let map = resources.get(fetchKey);
40
+ if (map === undefined) {
41
+ map = new Map();
42
+ resources.set(fetchKey, map);
43
+ }
44
+ return map;
45
+}
46
+
47
+// TODO: options, auth, etc.
48
+export function fetch(url: string): Object {
49
+ const map = readResultMap();
50
+ const entry = map.get(url);
51
+ if (entry === undefined) {
52
+ let resolve = () => {};
53
+ const wakeable: Wakeable = new Promise(r => {
54
+ // TODO: should this be a plain thenable instead?
55
+ resolve = r;
56
+ });
57
+ const result: Result = {
58
+ status: Pending,
59
+ value: wakeable,
60
+ };
61
+ map.set(url, result);
62
+ const xhr = new XMLHttpRequest();
63
+ xhr.onload = function() {
64
+ // TODO: should we handle status codes?
65
+ if (result.status !== Pending) {
66
+ return;
67
+ }
68
+ const resolvedResult = ((result: any): ResolvedResult);
69
+ resolvedResult.status = Resolved;
70
+ resolvedResult.value = xhr.response;
71
+ resolve();
72
+ };
73
+ xhr.onerror = function() {
74
+ if (result.status !== Pending) {
75
+ return;
76
+ }
77
+ const rejectedResult = ((result: any): RejectedResult);
78
+ rejectedResult.status = Rejected;
79
+ // TODO: use something else as the error value?
80
+ rejectedResult.value = xhr;
81
+ resolve();
82
+ };
83
+ xhr.open('GET', url);
84
+ xhr.send();
85
+ throw wakeable;
86
+ }
87
+ const result: Result = entry;
88
+ if (result.status === Resolved) {
89
+ return result.value;
90
+ } else {
91
+ throw result.value;
92
+ }
93
+}
packages/react/npm/unstable-cache.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-unstable-cache.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-unstable-cache.development.js');
7
+}
packages/react/package.json
+2
-1
@@ -16,7 +16,8 @@
16
"cjs/",
17
"umd/",
18
"jsx-runtime.js",
19
- "jsx-dev-runtime.js"
19
+ "jsx-dev-runtime.js",
20
+ "unstable-cache.js"
21
],
22
"main": "index.js",
23
"repository": {
packages/react/src/__tests__/ReactCache-test.js
new
+23
@@ -0,0 +1,23 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+describe('ReactCache', () => {
13
+ let ReactCache;
14
+
15
+ beforeEach(() => {
16
+ ReactCache = require('react/unstable-cache');
17
+ });
18
+
19
+ // TODO: test something useful.
20
+ it('exports something', () => {
21
+ expect(ReactCache.readCache).not.toBe(undefined);
22
+ });
23
+});
packages/react/src/cache/ReactCache.js
new
+43
@@ -0,0 +1,43 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ * @flow
7
+ */
8
+
9
+import type {ReactContext} from 'shared/ReactTypes';
10
+
11
+import {createContext} from 'react';
12
+import invariant from 'shared/invariant';
13
+
14
+type Cache = {|
15
+ resources: Map<any, any>,
16
+|};
17
+
18
+// TODO: should there be a default cache?
19
+const CacheContext: ReactContext<null | Cache> = createContext(null);
20
+
21
+function CacheImpl() {
22
+ this.resources = new Map();
23
+ // TODO: cancellation token.
24
+}
25
+
26
+function createCache(): Cache {
27
+ // $FlowFixMe
28
+ return new CacheImpl();
29
+}
30
+
31
+function readCache(): Cache {
32
+ // TODO: this doesn't subscribe.
33
+ // But we really want load context anyway.
34
+ const value = CacheContext._currentValue;
35
+ if (value instanceof CacheImpl) {
36
+ return value;
37
+ }
38
+ invariant(false, 'Could not read the cache.');
39
+}
40
+
41
+const CacheProvider = CacheContext.Provider;
42
+
43
+export {createCache, readCache, CacheProvider};
packages/react/unstable-cache.js
new
+9
@@ -0,0 +1,9 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+export {createCache, readCache, CacheProvider} from './src/cache/ReactCache';
scripts/error-codes/codes.json
+2
-1
@@ -352,5 +352,6 @@
352
"352": "React Blocks (and Lazy Components) are expected to be replaced by a compiler on the server. Try configuring your compiler set up and avoid using React.lazy inside of Blocks.",
353
"353": "A server block should never encode any other slots. This is a bug in React.",
354
"354": "getInspectorDataForViewAtPoint() is not available in production.",
355
- "355": "The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly."
355
+ "355": "The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.",
356
+ "356": "Could not read the cache."
357
}
scripts/rollup/bundles.js
+49
-1
@@ -108,6 +108,54 @@ const bundles = [
108
externals: ['react'],
109
},
110
111
+ /******* React Cache (experimental, new) *******/
112
+ {
113
+ bundleTypes: [
114
+ NODE_DEV,
115
+ NODE_PROD,
116
+ NODE_PROFILING,
117
+ FB_WWW_DEV,
118
+ FB_WWW_PROD,
119
+ FB_WWW_PROFILING,
120
+ ],
121
+ moduleType: ISOMORPHIC,
122
+ entry: 'react/unstable-cache',
123
+ global: 'ReactCache',
124
+ externals: ['react'],
125
+ },
126
+
127
+ /******* React Data (experimental, new) *******/
128
+ {
129
+ bundleTypes: [
130
+ NODE_DEV,
131
+ NODE_PROD,
132
+ NODE_PROFILING,
133
+ FB_WWW_DEV,
134
+ FB_WWW_PROD,
135
+ FB_WWW_PROFILING,
136
+ ],
137
+ moduleType: ISOMORPHIC,
138
+ entry: 'react-data',
139
+ global: 'ReactData',
140
+ externals: ['react'],
141
+ },
142
+
143
+ /******* React Data Fetch (experimental, new) *******/
144
+ {
145
+ bundleTypes: [
146
+ NODE_DEV,
147
+ NODE_PROD,
148
+ NODE_PROFILING,
149
+ FB_WWW_DEV,
150
+ FB_WWW_PROD,
151
+ FB_WWW_PROFILING,
152
+ ],
153
+ moduleType: ISOMORPHIC,
154
+ entry: 'react-data/fetch',
155
+ global: 'ReactDataFetch',
156
+ externals: ['react', 'react-data'],
157
+ },
158
+
159
/******* React DOM *******/
160
{
161
bundleTypes: [
@@ -529,7 +577,7 @@ const bundles = [
577
externals: [],
578
},
579
532
- /******* React Cache (experimental) *******/
580
+ /******* React Cache (experimental, old) *******/
581
{
582
bundleTypes: [
583
FB_WWW_DEV,