@samitouri / QOS-React-2 / commits / e23673b511

[Flight] Add getCacheForType() to the dispatcher (#20315)

* Remove react/unstable_cache We're probably going to make it available via the dispatcher. Let's remove this for now. * Add readContext() to the dispatcher On the server, it will be per-request. On the client, there will be some way to shadow it. For now, I provide it on the server, and throw on the client. * Use readContext() from react-fetch This makes it work on the server (but not on the client until we implement it there.) Updated the test to use Server Components. Now it passes. * Fixture: Add fetch from a Server Component * readCache -> getCacheForType<T> * Add React.unstable_getCacheForType * Add a feature flag * Fix Flow * Add react-suspense-test-utils and port tests * Remove extra Map lookup * Unroll async/await because build system * Add some error coverage and retry * Add unstable_getCacheForType to Flight entry

Dan Abramov committed Dec 3, 2020 at 03:44 UTC e23673b511a2eab6ddcb848a4150105c954f289a
37 files changed +363 -156
fixtures/flight/server/cli.server.js
+18 -3
@@ -17,13 +17,28 @@ const app = express();
17 // Application
18 app.get('/', function(req, res) {
19 if (process.env.NODE_ENV === 'development') {
20 - for (var key in require.cache) {
21 - delete require.cache[key];
22 - }
20 + // This doesn't work in ESM mode.
21 + // for (var key in require.cache) {
22 + // delete require.cache[key];
23 + // }
24 }
25 require('./handler.server.js')(req, res);
26 });
27
28 +app.get('/todos', function(req, res) {
29 + res.setHeader('Access-Control-Allow-Origin', '*');
30 + res.json([
31 + {
32 + id: 1,
33 + text: 'Shave yaks',
34 + },
35 + {
36 + id: 2,
37 + text: 'Eat kale',
38 + },
39 + ]);
40 +});
41 +
42 app.listen(3001, () => {
43 console.log('Flight Server listening on port 3001...');
44 });
fixtures/flight/src/App.server.js
+7
@@ -1,4 +1,5 @@
1 import * as React from 'react';
2 +import {fetch} from 'react-fetch';
3
4 import Container from './Container.js';
5
@@ -8,11 +9,17 @@ import {Counter as Counter2} from './Counter2.client.js';
9 import ShowMore from './ShowMore.client.js';
10
11 export default function App() {
12 + const todos = fetch('http://localhost:3001/todos').json();
13 return (
14 <Container>
15 <h1>Hello, world</h1>
16 <Counter />
17 <Counter2 />
18 + <ul>
19 + {todos.map(todo => (
20 + <li key={todo.id}>{todo.text}</li>
21 + ))}
22 + </ul>
23 <ShowMore>
24 <p>Lorem ipsum</p>
25 </ShowMore>
packages/react-debug-tools/src/ReactDebugHooks.js
+6
@@ -23,6 +23,7 @@ import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';
23 import {NoMode} from 'react-reconciler/src/ReactTypeOfMode';
24
25 import ErrorStackParser from 'error-stack-parser';
26 +import invariant from 'shared/invariant';
27 import ReactSharedInternals from 'shared/ReactSharedInternals';
28 import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';
29 import {
@@ -100,6 +101,10 @@ function nextHook(): null | Hook {
101 return hook;
102 }
103
104 +function getCacheForType<T>(resourceType: () => T): T {
105 + invariant(false, 'Not implemented.');
106 +}
107 +
108 function readContext<T>(
109 context: ReactContext<T>,
110 observedBits: void | number | boolean,
@@ -298,6 +303,7 @@ function useOpaqueIdentifier(): OpaqueIDType | void {
303 }
304
305 const Dispatcher: DispatcherType = {
306 + getCacheForType,
307 readContext,
308 useCallback,
309 useContext,
packages/react-dom/src/server/ReactPartialRendererHooks.js
+9
@@ -20,6 +20,7 @@ import type PartialRenderer from './ReactPartialRenderer';
20 import {validateContextBounds} from './ReactPartialRendererContext';
21
22 import invariant from 'shared/invariant';
23 +import {enableCache} from 'shared/ReactFeatureFlags';
24 import is from 'shared/objectIs';
25
26 type BasicStateAction<S> = (S => S) | S;
@@ -214,6 +215,10 @@ export function resetHooksState(): void {
215 workInProgressHook = null;
216 }
217
218 +function getCacheForType<T>(resourceType: () => T): T {
219 + invariant(false, 'Not implemented.');
220 +}
221 +
222 function readContext<T>(
223 context: ReactContext<T>,
224 observedBits: void | number | boolean,
@@ -512,3 +517,7 @@ export const Dispatcher: DispatcherType = {
517 // Subscriptions are not setup in a server environment.
518 useMutableSource,
519 };
520 +
521 +if (enableCache) {
522 + Dispatcher.getCacheForType = getCacheForType;
523 +}
packages/react-fetch/src/ReactFetchBrowser.js
+9 -12
@@ -9,7 +9,7 @@
9
10 import type {Wakeable} from 'shared/ReactTypes';
11
12 -import {readCache} from 'react/unstable-cache';
12 +import {unstable_getCacheForType} from 'react';
13
14 const Pending = 0;
15 const Resolved = 1;
@@ -34,16 +34,13 @@ type Result = PendingResult | ResolvedResult | RejectedResult;
34
35 // TODO: this is a browser-only version. Add a separate Node entry point.
36 const nativeFetch = window.fetch;
37 -const fetchKey = {};
38 -
39 -function readResultMap(): Map<string, Result> {
40 - const resources = readCache().resources;
41 - let map = resources.get(fetchKey);
42 - if (map === undefined) {
43 - map = new Map();
44 - resources.set(fetchKey, map);
45 - }
46 - return map;
37 +
38 +function getResultMap(): Map<string, Result> {
39 + return unstable_getCacheForType(createResultMap);
40 +}
41 +
42 +function createResultMap(): Map<string, Result> {
43 + return new Map();
44 }
45
46 function toResult(thenable): Result {
@@ -120,7 +117,7 @@ Response.prototype = {
117 };
118
119 function preloadResult(url: string, options: mixed): Result {
123 - const map = readResultMap();
120 + const map = getResultMap();
121 let entry = map.get(url);
122 if (!entry) {
123 if (options) {
packages/react-fetch/src/ReactFetchNode.js
+7 -12
@@ -11,8 +11,7 @@ import type {Wakeable} from 'shared/ReactTypes';
11
12 import * as http from 'http';
13 import * as https from 'https';
14 -
15 -import {readCache} from 'react/unstable-cache';
14 +import {unstable_getCacheForType} from 'react';
15
16 type FetchResponse = {|
17 // Properties
@@ -75,16 +74,12 @@ type RejectedResult = {|
74
75 type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
76
78 -const fetchKey = {};
77 +function getResultMap(): Map<string, Result<FetchResponse>> {
78 + return unstable_getCacheForType(createResultMap);
79 +}
80
80 -function readResultMap(): Map<string, Result<FetchResponse>> {
81 - const resources = readCache().resources;
82 - let map = resources.get(fetchKey);
83 - if (map === undefined) {
84 - map = new Map();
85 - resources.set(fetchKey, map);
86 - }
87 - return map;
81 +function createResultMap(): Map<string, Result<FetchResponse>> {
82 + return new Map();
83 }
84
85 function readResult<T>(result: Result<T>): T {
@@ -166,7 +161,7 @@ Response.prototype = {
161 };
162
163 function preloadResult(url: string, options: mixed): Result<FetchResponse> {
169 - const map = readResultMap();
164 + const map = getResultMap();
165 let entry = map.get(url);
166 if (!entry) {
167 if (options) {
packages/react-fetch/src/__tests__/ReactFetchNode-test.js
+73 -47
@@ -10,30 +10,28 @@
10 'use strict';
11
12 describe('ReactFetchNode', () => {
13 - let ReactCache;
14 - let ReactFetchNode;
13 let http;
14 let fetch;
15 + let waitForSuspense;
16 let server;
17 let serverEndpoint;
18 let serverImpl;
19
20 beforeEach(done => {
21 jest.resetModules();
23 - if (__EXPERIMENTAL__) {
24 - ReactCache = require('react/unstable-cache');
25 - // TODO: A way to pass load context.
26 - ReactCache.CacheProvider._context._currentValue = ReactCache.createCache();
27 - ReactFetchNode = require('react-fetch');
28 - fetch = ReactFetchNode.fetch;
29 - }
22 +
23 + fetch = require('react-fetch').fetch;
24 http = require('http');
25 + waitForSuspense = require('react-suspense-test-utils').waitForSuspense;
26
27 server = http.createServer((req, res) => {
28 serverImpl(req, res);
29 });
35 - server.listen(done);
36 - serverEndpoint = `http://localhost:${server.address().port}/`;
30 + serverEndpoint = null;
31 + server.listen(() => {
32 + serverEndpoint = `http://localhost:${server.address().port}/`;
33 + done();
34 + });
35 });
36
37 afterEach(done => {
@@ -41,55 +39,83 @@ describe('ReactFetchNode', () => {
39 server = null;
40 });
41
44 - async function waitForSuspense(fn) {
45 - while (true) {
46 - try {
47 - return fn();
48 - } catch (promise) {
49 - if (typeof promise.then === 'function') {
50 - await promise;
51 - } else {
52 - throw promise;
53 - }
54 - }
55 - }
56 - }
42 + // @gate experimental
43 + it('can fetch text from a server component', async () => {
44 + serverImpl = (req, res) => {
45 + res.write('mango');
46 + res.end();
47 + };
48 + const text = await waitForSuspense(() => {
49 + return fetch(serverEndpoint).text();
50 + });
51 + expect(text).toEqual('mango');
52 + });
53
54 // @gate experimental
59 - it('can read text', async () => {
55 + it('can fetch json from a server component', async () => {
56 serverImpl = (req, res) => {
61 - res.write('ok');
57 + res.write(JSON.stringify({name: 'Sema'}));
58 res.end();
59 };
64 - await waitForSuspense(() => {
65 - const response = fetch(serverEndpoint);
66 - expect(response.status).toBe(200);
67 - expect(response.statusText).toBe('OK');
68 - expect(response.ok).toBe(true);
69 - expect(response.text()).toEqual('ok');
70 - // Can read again:
71 - expect(response.text()).toEqual('ok');
60 + const json = await waitForSuspense(() => {
61 + return fetch(serverEndpoint).json();
62 });
63 + expect(json).toEqual({name: 'Sema'});
64 });
65
66 // @gate experimental
76 - it('can read json', async () => {
67 + it('provides response status', async () => {
68 serverImpl = (req, res) => {
69 res.write(JSON.stringify({name: 'Sema'}));
70 res.end();
71 };
81 - await waitForSuspense(() => {
82 - const response = fetch(serverEndpoint);
83 - expect(response.status).toBe(200);
84 - expect(response.statusText).toBe('OK');
85 - expect(response.ok).toBe(true);
86 - expect(response.json()).toEqual({
87 - name: 'Sema',
88 - });
89 - // Can read again:
90 - expect(response.json()).toEqual({
91 - name: 'Sema',
92 - });
72 + const response = await waitForSuspense(() => {
73 + return fetch(serverEndpoint);
74 + });
75 + expect(response).toMatchObject({
76 + status: 200,
77 + statusText: 'OK',
78 + ok: true,
79 });
80 });
81 +
82 + // @gate experimental
83 + it('handles different paths', async () => {
84 + serverImpl = (req, res) => {
85 + switch (req.url) {
86 + case '/banana':
87 + res.write('banana');
88 + break;
89 + case '/mango':
90 + res.write('mango');
91 + break;
92 + case '/orange':
93 + res.write('orange');
94 + break;
95 + }
96 + res.end();
97 + };
98 + const outputs = await waitForSuspense(() => {
99 + return [
100 + fetch(serverEndpoint + 'banana').text(),
101 + fetch(serverEndpoint + 'mango').text(),
102 + fetch(serverEndpoint + 'orange').text(),
103 + ];
104 + });
105 + expect(outputs).toMatchObject(['banana', 'mango', 'orange']);
106 + });
107 +
108 + // @gate experimental
109 + it('can produce an error', async () => {
110 + serverImpl = (req, res) => {};
111 +
112 + expect.assertions(1);
113 + try {
114 + await waitForSuspense(() => {
115 + return fetch('BOOM');
116 + });
117 + } catch (err) {
118 + expect(err.message).toEqual('Invalid URL: BOOM');
119 + }
120 + });
121 });
packages/react-reconciler/src/ReactFiberHooks.new.js
+35
@@ -25,6 +25,7 @@ import {
25 enableDebugTracing,
26 enableSchedulingProfiler,
27 enableNewReconciler,
28 + enableCache,
29 decoupleUpdatePriorityFromScheduler,
30 enableUseRefAccessWarning,
31 } from 'shared/ReactFeatureFlags';
@@ -1815,6 +1816,10 @@ function dispatchAction<S, A>(
1816 }
1817 }
1818
1819 +function getCacheForType<T>(resourceType: () => T): T {
1820 + invariant(false, 'Not implemented.');
1821 +}
1822 +
1823 export const ContextOnlyDispatcher: Dispatcher = {
1824 readContext,
1825
@@ -1835,6 +1840,9 @@ export const ContextOnlyDispatcher: Dispatcher = {
1840
1841 unstable_isNewReconciler: enableNewReconciler,
1842 };
1843 +if (enableCache) {
1844 + (ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
1845 +}
1846
1847 const HooksDispatcherOnMount: Dispatcher = {
1848 readContext,
@@ -1877,6 +1885,9 @@ const HooksDispatcherOnUpdate: Dispatcher = {
1885
1886 unstable_isNewReconciler: enableNewReconciler,
1887 };
1888 +if (enableCache) {
1889 + (HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
1890 +}
1891
1892 const HooksDispatcherOnRerender: Dispatcher = {
1893 readContext,
@@ -1898,6 +1909,9 @@ const HooksDispatcherOnRerender: Dispatcher = {
1909
1910 unstable_isNewReconciler: enableNewReconciler,
1911 };
1912 +if (enableCache) {
1913 + (HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
1914 +}
1915
1916 let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
1917 let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@@ -2052,6 +2066,9 @@ if (__DEV__) {
2066
2067 unstable_isNewReconciler: enableNewReconciler,
2068 };
2069 + if (enableCache) {
2070 + (HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
2071 + }
2072
2073 HooksDispatcherOnMountWithHookTypesInDEV = {
2074 readContext<T>(
@@ -2174,6 +2191,9 @@ if (__DEV__) {
2191
2192 unstable_isNewReconciler: enableNewReconciler,
2193 };
2194 + if (enableCache) {
2195 + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType;
2196 + }
2197
2198 HooksDispatcherOnUpdateInDEV = {
2199 readContext<T>(
@@ -2296,6 +2316,9 @@ if (__DEV__) {
2316
2317 unstable_isNewReconciler: enableNewReconciler,
2318 };
2319 + if (enableCache) {
2320 + (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
2321 + }
2322
2323 HooksDispatcherOnRerenderInDEV = {
2324 readContext<T>(
@@ -2419,6 +2442,9 @@ if (__DEV__) {
2442
2443 unstable_isNewReconciler: enableNewReconciler,
2444 };
2445 + if (enableCache) {
2446 + (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
2447 + }
2448
2449 InvalidNestedHooksDispatcherOnMountInDEV = {
2450 readContext<T>(
@@ -2556,6 +2582,9 @@ if (__DEV__) {
2582
2583 unstable_isNewReconciler: enableNewReconciler,
2584 };
2585 + if (enableCache) {
2586 + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
2587 + }
2588
2589 InvalidNestedHooksDispatcherOnUpdateInDEV = {
2590 readContext<T>(
@@ -2693,6 +2722,9 @@ if (__DEV__) {
2722
2723 unstable_isNewReconciler: enableNewReconciler,
2724 };
2725 + if (enableCache) {
2726 + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
2727 + }
2728
2729 InvalidNestedHooksDispatcherOnRerenderInDEV = {
2730 readContext<T>(
@@ -2831,4 +2863,7 @@ if (__DEV__) {
2863
2864 unstable_isNewReconciler: enableNewReconciler,
2865 };
2866 + if (enableCache) {
2867 + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
2868 + }
2869 }
packages/react-reconciler/src/ReactFiberHooks.old.js
+38
@@ -25,6 +25,7 @@ import {
25 enableDebugTracing,
26 enableSchedulingProfiler,
27 enableNewReconciler,
28 + enableCache,
29 decoupleUpdatePriorityFromScheduler,
30 enableUseRefAccessWarning,
31 } from 'shared/ReactFeatureFlags';
@@ -1815,6 +1816,10 @@ function dispatchAction<S, A>(
1816 }
1817 }
1818
1819 +function getCacheForType<T>(resourceType: () => T): T {
1820 + invariant(false, 'Not implemented.');
1821 +}
1822 +
1823 export const ContextOnlyDispatcher: Dispatcher = {
1824 readContext,
1825
@@ -1835,6 +1840,9 @@ export const ContextOnlyDispatcher: Dispatcher = {
1840
1841 unstable_isNewReconciler: enableNewReconciler,
1842 };
1843 +if (enableCache) {
1844 + (ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
1845 +}
1846
1847 const HooksDispatcherOnMount: Dispatcher = {
1848 readContext,
@@ -1856,6 +1864,9 @@ const HooksDispatcherOnMount: Dispatcher = {
1864
1865 unstable_isNewReconciler: enableNewReconciler,
1866 };
1867 +if (enableCache) {
1868 + (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
1869 +}
1870
1871 const HooksDispatcherOnUpdate: Dispatcher = {
1872 readContext,
@@ -1877,6 +1888,9 @@ const HooksDispatcherOnUpdate: Dispatcher = {
1888
1889 unstable_isNewReconciler: enableNewReconciler,
1890 };
1891 +if (enableCache) {
1892 + (HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
1893 +}
1894
1895 const HooksDispatcherOnRerender: Dispatcher = {
1896 readContext,
@@ -1898,6 +1912,9 @@ const HooksDispatcherOnRerender: Dispatcher = {
1912
1913 unstable_isNewReconciler: enableNewReconciler,
1914 };
1915 +if (enableCache) {
1916 + (HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
1917 +}
1918
1919 let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
1920 let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@@ -2052,6 +2069,9 @@ if (__DEV__) {
2069
2070 unstable_isNewReconciler: enableNewReconciler,
2071 };
2072 + if (enableCache) {
2073 + (HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
2074 + }
2075
2076 HooksDispatcherOnMountWithHookTypesInDEV = {
2077 readContext<T>(
@@ -2174,6 +2194,9 @@ if (__DEV__) {
2194
2195 unstable_isNewReconciler: enableNewReconciler,
2196 };
2197 + if (enableCache) {
2198 + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType;
2199 + }
2200
2201 HooksDispatcherOnUpdateInDEV = {
2202 readContext<T>(
@@ -2296,6 +2319,9 @@ if (__DEV__) {
2319
2320 unstable_isNewReconciler: enableNewReconciler,
2321 };
2322 + if (enableCache) {
2323 + (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
2324 + }
2325
2326 HooksDispatcherOnRerenderInDEV = {
2327 readContext<T>(
@@ -2419,6 +2445,9 @@ if (__DEV__) {
2445
2446 unstable_isNewReconciler: enableNewReconciler,
2447 };
2448 + if (enableCache) {
2449 + (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
2450 + }
2451
2452 InvalidNestedHooksDispatcherOnMountInDEV = {
2453 readContext<T>(
@@ -2556,6 +2585,9 @@ if (__DEV__) {
2585
2586 unstable_isNewReconciler: enableNewReconciler,
2587 };
2588 + if (enableCache) {
2589 + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
2590 + }
2591
2592 InvalidNestedHooksDispatcherOnUpdateInDEV = {
2593 readContext<T>(
@@ -2693,6 +2725,9 @@ if (__DEV__) {
2725
2726 unstable_isNewReconciler: enableNewReconciler,
2727 };
2728 + if (enableCache) {
2729 + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
2730 + }
2731
2732 InvalidNestedHooksDispatcherOnRerenderInDEV = {
2733 readContext<T>(
@@ -2831,4 +2866,7 @@ if (__DEV__) {
2866
2867 unstable_isNewReconciler: enableNewReconciler,
2868 };
2869 + if (enableCache) {
2870 + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
2871 + }
2872 }
packages/react-reconciler/src/ReactInternalTypes.js
+1
@@ -274,6 +274,7 @@ type BasicStateAction<S> = (S => S) | S;
274 type Dispatch<A> = A => void;
275
276 export type Dispatcher = {|
277 + getCacheForType?: <T>(resourceType: () => T) => T,
278 readContext<T>(
279 context: ReactContext<T>,
280 observedBits: void | number | boolean,
packages/react-server/src/ReactFlightServer.js
+20
@@ -74,6 +74,7 @@ type Segment = {
74 export type Request = {
75 destination: Destination,
76 bundlerConfig: BundlerConfig,
77 + cache: Map<Function, mixed>,
78 nextChunkId: number,
79 pendingChunks: number,
80 pingedSegments: Array<Segment>,
@@ -97,6 +98,7 @@ export function createRequest(
98 const request = {
99 destination,
100 bundlerConfig,
101 + cache: new Map(),
102 nextChunkId: 0,
103 pendingChunks: 0,
104 pingedSegments: pingedSegments,
@@ -652,7 +654,9 @@ function retrySegment(request: Request, segment: Segment): void {
654
655 function performWork(request: Request): void {
656 const prevDispatcher = ReactCurrentDispatcher.current;
657 + const prevCache = currentCache;
658 ReactCurrentDispatcher.current = Dispatcher;
659 + currentCache = request.cache;
660
661 const pingedSegments = request.pingedSegments;
662 request.pingedSegments = [];
@@ -665,6 +669,7 @@ function performWork(request: Request): void {
669 }
670
671 ReactCurrentDispatcher.current = prevDispatcher;
672 + currentCache = prevCache;
673 }
674
675 let reentrant = false;
@@ -743,6 +748,8 @@ function unsupportedHook(): void {
748 invariant(false, 'This Hook is not supported in Server Components.');
749 }
750
751 +let currentCache: Map<Function, mixed> | null = null;
752 +
753 const Dispatcher: DispatcherType = {
754 useMemo<T>(nextCreate: () => T): T {
755 return nextCreate();
@@ -757,6 +764,19 @@ const Dispatcher: DispatcherType = {
764 useTransition(): [(callback: () => void) => void, boolean] {
765 return [() => {}, false];
766 },
767 + getCacheForType<T>(resourceType: () => T): T {
768 + invariant(
769 + currentCache,
770 + 'Reading the cache is only supported while rendering.',
771 + );
772 + let entry: T | void = (currentCache.get(resourceType): any);
773 + if (entry === undefined) {
774 + entry = resourceType();
775 + // TODO: Warn if undefined?
776 + currentCache.set(resourceType, entry);
777 + }
778 + return entry;
779 + },
780 readContext: (unsupportedHook: any),
781 useContext: (unsupportedHook: any),
782 useReducer: (unsupportedHook: any),
packages/react-suspense-test-utils/README.md new
+12
@@ -0,0 +1,12 @@
1 +# react-suspense-test-utils
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-suspense-test-utils/index.js renamed
+2 -1
@@ -6,4 +6,5 @@
6 *
7 * @flow
8 */
9 -export {createCache, readCache, CacheProvider} from './src/cache/ReactCache';
9 +
10 +export * from './src/ReactSuspenseTestUtils';
packages/react-suspense-test-utils/npm/index.js new
+3
@@ -0,0 +1,3 @@
1 +'use strict';
2 +
3 +module.exports = require('./cjs/react-suspense-test-utils.js');
packages/react-suspense-test-utils/package.json new
+20
@@ -0,0 +1,20 @@
1 +{
2 + "name": "react-suspense-test-utils",
3 + "version": "0.1.0",
4 + "private": true,
5 + "repository": {
6 + "type" : "git",
7 + "url" : "https://github.com/facebook/react.git",
8 + "directory": "packages/react-suspense-test-utils"
9 + },
10 + "license": "MIT",
11 + "files": [
12 + "LICENSE",
13 + "README.md",
14 + "index.js",
15 + "cjs/"
16 + ],
17 + "peerDependencies": {
18 + "react": "^17.0.0"
19 + }
20 +}
packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js new
+68
@@ -0,0 +1,68 @@
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 {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
11 +import ReactSharedInternals from 'shared/ReactSharedInternals';
12 +import invariant from 'shared/invariant';
13 +
14 +const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
15 +
16 +function unsupported() {
17 + invariant(false, 'This feature is not supported by ReactSuspenseTestUtils.');
18 +}
19 +
20 +export function waitForSuspense<T>(fn: () => T): Promise<T> {
21 + const cache: Map<Function, mixed> = new Map();
22 + const testDispatcher: Dispatcher = {
23 + getCacheForType<R>(resourceType: () => R): R {
24 + let entry: R | void = (cache.get(resourceType): any);
25 + if (entry === undefined) {
26 + entry = resourceType();
27 + // TODO: Warn if undefined?
28 + cache.set(resourceType, entry);
29 + }
30 + return entry;
31 + },
32 + readContext: unsupported,
33 + useContext: unsupported,
34 + useMemo: unsupported,
35 + useReducer: unsupported,
36 + useRef: unsupported,
37 + useState: unsupported,
38 + useLayoutEffect: unsupported,
39 + useCallback: unsupported,
40 + useImperativeHandle: unsupported,
41 + useEffect: unsupported,
42 + useDebugValue: unsupported,
43 + useDeferredValue: unsupported,
44 + useTransition: unsupported,
45 + useOpaqueIdentifier: unsupported,
46 + useMutableSource: unsupported,
47 + };
48 + // Not using async/await because we don't compile it.
49 + return new Promise((resolve, reject) => {
50 + function retry() {
51 + const prevDispatcher = ReactCurrentDispatcher.current;
52 + ReactCurrentDispatcher.current = testDispatcher;
53 + try {
54 + const result = fn();
55 + resolve(result);
56 + } catch (thrownValue) {
57 + if (typeof thrownValue.then === 'function') {
58 + thrownValue.then(retry, retry);
59 + } else {
60 + reject(thrownValue);
61 + }
62 + } finally {
63 + ReactCurrentDispatcher.current = prevDispatcher;
64 + }
65 + }
66 + retry();
67 + });
68 +}
packages/react/index.classic.fb.js
+1
@@ -50,6 +50,7 @@ export {
50 startTransition as unstable_startTransition,
51 SuspenseList,
52 SuspenseList as unstable_SuspenseList,
53 + unstable_getCacheForType,
54 // enableScopeAPI
55 unstable_Scope,
56 unstable_useOpaqueIdentifier,
packages/react/index.experimental.js
+1
@@ -45,6 +45,7 @@ export {
45 startTransition as unstable_startTransition,
46 SuspenseList as unstable_SuspenseList,
47 unstable_useOpaqueIdentifier,
48 + unstable_getCacheForType,
49 // enableDebugTracing
50 unstable_DebugTracingMode,
51 } from './src/React';
packages/react/index.js
+1
@@ -82,4 +82,5 @@ export {
82 unstable_createFundamental,
83 unstable_Scope,
84 unstable_useOpaqueIdentifier,
85 + unstable_getCacheForType,
86 } from './src/React';
packages/react/index.modern.fb.js
+1
@@ -49,6 +49,7 @@ export {
49 startTransition as unstable_startTransition,
50 SuspenseList,
51 SuspenseList as unstable_SuspenseList,
52 + unstable_getCacheForType,
53 // enableScopeAPI
54 unstable_Scope,
55 unstable_useOpaqueIdentifier,
packages/react/package.json
+1 -2
@@ -17,8 +17,7 @@
17 "umd/",
18 "jsx-runtime.js",
19 "jsx-dev-runtime.js",
20 - "unstable-index.server.js",
21 - "unstable-cache.js"
20 + "unstable-index.server.js"
21 ],
22 "main": "index.js",
23 "exports": {
packages/react/src/React.js
+2
@@ -33,6 +33,7 @@ import {lazy} from './ReactLazy';
33 import {forwardRef} from './ReactForwardRef';
34 import {memo} from './ReactMemo';
35 import {
36 + getCacheForType,
37 useCallback,
38 useContext,
39 useEffect,
@@ -110,6 +111,7 @@ export {
111 useDeferredValue,
112 REACT_SUSPENSE_LIST_TYPE as SuspenseList,
113 REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
114 + getCacheForType as unstable_getCacheForType,
115 // enableFundamentalAPI
116 createFundamental as unstable_createFundamental,
117 // enableScopeAPI
packages/react/src/ReactHooks.js
+6
@@ -36,6 +36,12 @@ function resolveDispatcher() {
36 return dispatcher;
37 }
38
39 +export function getCacheForType<T>(resourceType: () => T): T {
40 + const dispatcher = resolveDispatcher();
41 + // $FlowFixMe This is unstable, thus optional
42 + return dispatcher.getCacheForType(resourceType);
43 +}
44 +
45 export function useContext<T>(
46 Context: ReactContext<T>,
47 unstable_observedBits: number | boolean | void,
packages/react/src/__tests__/ReactCache-test.js deleted
-26
@@ -1,26 +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 - * @emails react-core
8 - */
9 -
10 -'use strict';
11 -
12 -describe('ReactCache', () => {
13 - let ReactCache;
14 -
15 - beforeEach(() => {
16 - if (__EXPERIMENTAL__) {
17 - ReactCache = require('react/unstable-cache');
18 - }
19 - });
20 -
21 - // TODO: test something useful.
22 - // @gate experimental
23 - it('exports something', () => {
24 - expect(ReactCache.readCache).not.toBe(undefined);
25 - });
26 -});
packages/react/src/cache/ReactCache.js deleted
-43
@@ -1,43 +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 - * @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-index.server.experimental.js
+1
@@ -32,6 +32,7 @@ export {
32 useDeferredValue as unstable_useDeferredValue,
33 SuspenseList as unstable_SuspenseList,
34 unstable_useOpaqueIdentifier,
35 + unstable_getCacheForType,
36 // enableDebugTracing
37 unstable_DebugTracingMode,
38 } from './src/React';
packages/shared/ReactFeatureFlags.js
+1
@@ -52,6 +52,7 @@ export const enableSelectiveHydration = __EXPERIMENTAL__;
52
53 // Flight experiments
54 export const enableLazyElements = __EXPERIMENTAL__;
55 +export const enableCache = __EXPERIMENTAL__;
56
57 // Only used in www builds.
58 export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -21,6 +21,7 @@ export const enableSchedulerTracing = __PROFILE__;
21 export const enableSuspenseServerRenderer = false;
22 export const enableSelectiveHydration = false;
23 export const enableLazyElements = false;
24 +export const enableCache = false;
25 export const enableSchedulerDebugging = false;
26 export const debugRenderPhaseSideEffectsForStrictMode = true;
27 export const disableJavaScriptURLs = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -23,6 +23,7 @@ export const enableSchedulerTracing = __PROFILE__;
23 export const enableSuspenseServerRenderer = false;
24 export const enableSelectiveHydration = false;
25 export const enableLazyElements = false;
26 +export const enableCache = false;
27 export const disableJavaScriptURLs = false;
28 export const disableInputAttributeSyncing = false;
29 export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -23,6 +23,7 @@ export const enableSchedulerTracing = __PROFILE__;
23 export const enableSuspenseServerRenderer = false;
24 export const enableSelectiveHydration = false;
25 export const enableLazyElements = false;
26 +export const enableCache = false;
27 export const disableJavaScriptURLs = false;
28 export const disableInputAttributeSyncing = false;
29 export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -23,6 +23,7 @@ export const enableSchedulerTracing = __PROFILE__;
23 export const enableSuspenseServerRenderer = false;
24 export const enableSelectiveHydration = false;
25 export const enableLazyElements = false;
26 +export const enableCache = false;
27 export const disableJavaScriptURLs = false;
28 export const disableInputAttributeSyncing = false;
29 export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -23,6 +23,7 @@ export const enableSchedulerTracing = __PROFILE__;
23 export const enableSuspenseServerRenderer = false;
24 export const enableSelectiveHydration = false;
25 export const enableLazyElements = false;
26 +export const enableCache = false;
27 export const enableSchedulerDebugging = false;
28 export const disableJavaScriptURLs = false;
29 export const disableInputAttributeSyncing = false;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -23,6 +23,7 @@ export const enableSchedulerTracing = __PROFILE__;
23 export const enableSuspenseServerRenderer = false;
24 export const enableSelectiveHydration = false;
25 export const enableLazyElements = false;
26 +export const enableCache = false;
27 export const disableJavaScriptURLs = false;
28 export const disableInputAttributeSyncing = false;
29 export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -23,6 +23,7 @@ export const enableSchedulerTracing = false;
23 export const enableSuspenseServerRenderer = true;
24 export const enableSelectiveHydration = true;
25 export const enableLazyElements = false;
26 +export const enableCache = false;
27 export const disableJavaScriptURLs = true;
28 export const disableInputAttributeSyncing = false;
29 export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -59,6 +59,7 @@ export const enableSuspenseServerRenderer = true;
59 export const enableSelectiveHydration = true;
60
61 export const enableLazyElements = true;
62 +export const enableCache = true;
63
64 export const disableJavaScriptURLs = true;
65
scripts/error-codes/codes.json
+3 -1
@@ -367,5 +367,7 @@
367 "376": "Only global symbols received from Symbol.for(...) can be passed to client components. The symbol Symbol.for(%s) cannot be found among global symbols. Remove %s from this object, or avoid the entire object: %s",
368 "377": "BigInt (%s) is not yet supported in client component props. Remove %s from this object or use a plain number instead: %s",
369 "378": "Type %s is not supported in client component props. Remove %s from this object, or avoid the entire object: %s",
370 - "379": "Refs cannot be used in server components, nor passed to client components."
370 + "379": "Refs cannot be used in server components, nor passed to client components.",
371 + "380": "Reading the cache is only supported while rendering.",
372 + "381": "This feature is not supported by ReactSuspenseTestUtils."
373 }
scripts/rollup/bundles.js
+9 -9
@@ -135,15 +135,6 @@ const bundles = [
135 externals: ['react'],
136 },
137
138 - /******* React Cache (experimental, new) *******/
139 - {
140 - bundleTypes: __EXPERIMENTAL__ ? [NODE_DEV, NODE_PROD, NODE_PROFILING] : [],
141 - moduleType: ISOMORPHIC,
142 - entry: 'react/unstable-cache',
143 - global: 'ReactCache',
144 - externals: ['react'],
145 - },
146 -
138 /******* React Fetch Browser (experimental, new) *******/
139 {
140 bundleTypes: [NODE_DEV, NODE_PROD],
@@ -366,6 +357,15 @@ const bundles = [
357 ],
358 },
359
360 + /******* React Suspense Test Utils *******/
361 + {
362 + bundleTypes: [NODE_ES2015],
363 + moduleType: RENDERER_UTILS,
364 + entry: 'react-suspense-test-utils',
365 + global: 'ReactSuspenseTestUtils',
366 + externals: ['react'],
367 + },
368 +
369 /******* React ART *******/
370 {
371 bundleTypes: [