@samitouri / QOS-React-2 / commits / 515326753b

[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,