From e23673b511a2eab6ddcb848a4150105c954f289a Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 3 Dec 2020 03:44:56 +0000 Subject: [PATCH] [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 * 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 --- fixtures/flight/server/cli.server.js | 21 ++- fixtures/flight/src/App.server.js | 7 + .../react-debug-tools/src/ReactDebugHooks.js | 6 + .../src/server/ReactPartialRendererHooks.js | 9 ++ packages/react-fetch/src/ReactFetchBrowser.js | 21 ++- packages/react-fetch/src/ReactFetchNode.js | 19 +-- .../src/__tests__/ReactFetchNode-test.js | 120 +++++++++++------- .../src/ReactFiberHooks.new.js | 35 +++++ .../src/ReactFiberHooks.old.js | 38 ++++++ .../src/ReactInternalTypes.js | 1 + .../react-server/src/ReactFlightServer.js | 20 +++ packages/react-suspense-test-utils/README.md | 12 ++ .../index.js} | 3 +- .../react-suspense-test-utils/npm/index.js | 3 + .../react-suspense-test-utils/package.json | 20 +++ .../src/ReactSuspenseTestUtils.js | 68 ++++++++++ packages/react/index.classic.fb.js | 1 + packages/react/index.experimental.js | 1 + packages/react/index.js | 1 + packages/react/index.modern.fb.js | 1 + packages/react/package.json | 3 +- packages/react/src/React.js | 2 + packages/react/src/ReactHooks.js | 6 + .../react/src/__tests__/ReactCache-test.js | 26 ---- packages/react/src/cache/ReactCache.js | 43 ------- .../unstable-index.server.experimental.js | 1 + packages/shared/ReactFeatureFlags.js | 1 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.native.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.testing.js | 1 + .../forks/ReactFeatureFlags.testing.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + scripts/error-codes/codes.json | 4 +- scripts/rollup/bundles.js | 18 +-- 37 files changed, 363 insertions(+), 156 deletions(-) create mode 100644 packages/react-suspense-test-utils/README.md rename packages/{react/unstable-cache.js => react-suspense-test-utils/index.js} (72%) create mode 100644 packages/react-suspense-test-utils/npm/index.js create mode 100644 packages/react-suspense-test-utils/package.json create mode 100644 packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js delete mode 100644 packages/react/src/__tests__/ReactCache-test.js delete mode 100644 packages/react/src/cache/ReactCache.js diff --git a/fixtures/flight/server/cli.server.js b/fixtures/flight/server/cli.server.js index 00dc4815b7287..3d28e818bcf83 100644 --- a/fixtures/flight/server/cli.server.js +++ b/fixtures/flight/server/cli.server.js @@ -17,13 +17,28 @@ const app = express(); // Application app.get('/', function(req, res) { if (process.env.NODE_ENV === 'development') { - for (var key in require.cache) { - delete require.cache[key]; - } + // This doesn't work in ESM mode. + // for (var key in require.cache) { + // delete require.cache[key]; + // } } require('./handler.server.js')(req, res); }); +app.get('/todos', function(req, res) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.json([ + { + id: 1, + text: 'Shave yaks', + }, + { + id: 2, + text: 'Eat kale', + }, + ]); +}); + app.listen(3001, () => { console.log('Flight Server listening on port 3001...'); }); diff --git a/fixtures/flight/src/App.server.js b/fixtures/flight/src/App.server.js index 35a223dce8b07..4a22318e9f18d 100644 --- a/fixtures/flight/src/App.server.js +++ b/fixtures/flight/src/App.server.js @@ -1,4 +1,5 @@ import * as React from 'react'; +import {fetch} from 'react-fetch'; import Container from './Container.js'; @@ -8,11 +9,17 @@ import {Counter as Counter2} from './Counter2.client.js'; import ShowMore from './ShowMore.client.js'; export default function App() { + const todos = fetch('http://localhost:3001/todos').json(); return (

Hello, world

+
    + {todos.map(todo => ( +
  • {todo.text}
  • + ))} +

Lorem ipsum

diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 39f8f5118c265..ce62b5ce8b9ef 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -23,6 +23,7 @@ import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig'; import {NoMode} from 'react-reconciler/src/ReactTypeOfMode'; import ErrorStackParser from 'error-stack-parser'; +import invariant from 'shared/invariant'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols'; import { @@ -100,6 +101,10 @@ function nextHook(): null | Hook { return hook; } +function getCacheForType(resourceType: () => T): T { + invariant(false, 'Not implemented.'); +} + function readContext( context: ReactContext, observedBits: void | number | boolean, @@ -298,6 +303,7 @@ function useOpaqueIdentifier(): OpaqueIDType | void { } const Dispatcher: DispatcherType = { + getCacheForType, readContext, useCallback, useContext, diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js index 49ef413357e7d..3a543aa337b6c 100644 --- a/packages/react-dom/src/server/ReactPartialRendererHooks.js +++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js @@ -20,6 +20,7 @@ import type PartialRenderer from './ReactPartialRenderer'; import {validateContextBounds} from './ReactPartialRendererContext'; import invariant from 'shared/invariant'; +import {enableCache} from 'shared/ReactFeatureFlags'; import is from 'shared/objectIs'; type BasicStateAction = (S => S) | S; @@ -214,6 +215,10 @@ export function resetHooksState(): void { workInProgressHook = null; } +function getCacheForType(resourceType: () => T): T { + invariant(false, 'Not implemented.'); +} + function readContext( context: ReactContext, observedBits: void | number | boolean, @@ -512,3 +517,7 @@ export const Dispatcher: DispatcherType = { // Subscriptions are not setup in a server environment. useMutableSource, }; + +if (enableCache) { + Dispatcher.getCacheForType = getCacheForType; +} diff --git a/packages/react-fetch/src/ReactFetchBrowser.js b/packages/react-fetch/src/ReactFetchBrowser.js index af073733b8286..a83c8f5a6236c 100644 --- a/packages/react-fetch/src/ReactFetchBrowser.js +++ b/packages/react-fetch/src/ReactFetchBrowser.js @@ -9,7 +9,7 @@ import type {Wakeable} from 'shared/ReactTypes'; -import {readCache} from 'react/unstable-cache'; +import {unstable_getCacheForType} from 'react'; const Pending = 0; const Resolved = 1; @@ -34,16 +34,13 @@ type Result = PendingResult | ResolvedResult | RejectedResult; // TODO: this is a browser-only version. Add a separate Node entry point. const nativeFetch = window.fetch; -const fetchKey = {}; - -function readResultMap(): Map { - const resources = readCache().resources; - let map = resources.get(fetchKey); - if (map === undefined) { - map = new Map(); - resources.set(fetchKey, map); - } - return map; + +function getResultMap(): Map { + return unstable_getCacheForType(createResultMap); +} + +function createResultMap(): Map { + return new Map(); } function toResult(thenable): Result { @@ -120,7 +117,7 @@ Response.prototype = { }; function preloadResult(url: string, options: mixed): Result { - const map = readResultMap(); + const map = getResultMap(); let entry = map.get(url); if (!entry) { if (options) { diff --git a/packages/react-fetch/src/ReactFetchNode.js b/packages/react-fetch/src/ReactFetchNode.js index e082fbaf37f07..c6260d863fc59 100644 --- a/packages/react-fetch/src/ReactFetchNode.js +++ b/packages/react-fetch/src/ReactFetchNode.js @@ -11,8 +11,7 @@ import type {Wakeable} from 'shared/ReactTypes'; import * as http from 'http'; import * as https from 'https'; - -import {readCache} from 'react/unstable-cache'; +import {unstable_getCacheForType} from 'react'; type FetchResponse = {| // Properties @@ -75,16 +74,12 @@ type RejectedResult = {| type Result = PendingResult | ResolvedResult | RejectedResult; -const fetchKey = {}; +function getResultMap(): Map> { + return unstable_getCacheForType(createResultMap); +} -function readResultMap(): Map> { - const resources = readCache().resources; - let map = resources.get(fetchKey); - if (map === undefined) { - map = new Map(); - resources.set(fetchKey, map); - } - return map; +function createResultMap(): Map> { + return new Map(); } function readResult(result: Result): T { @@ -166,7 +161,7 @@ Response.prototype = { }; function preloadResult(url: string, options: mixed): Result { - const map = readResultMap(); + const map = getResultMap(); let entry = map.get(url); if (!entry) { if (options) { diff --git a/packages/react-fetch/src/__tests__/ReactFetchNode-test.js b/packages/react-fetch/src/__tests__/ReactFetchNode-test.js index 1588554972691..6ad5fdc9c434f 100644 --- a/packages/react-fetch/src/__tests__/ReactFetchNode-test.js +++ b/packages/react-fetch/src/__tests__/ReactFetchNode-test.js @@ -10,30 +10,28 @@ 'use strict'; describe('ReactFetchNode', () => { - let ReactCache; - let ReactFetchNode; let http; let fetch; + let waitForSuspense; let server; let serverEndpoint; let serverImpl; beforeEach(done => { jest.resetModules(); - if (__EXPERIMENTAL__) { - ReactCache = require('react/unstable-cache'); - // TODO: A way to pass load context. - ReactCache.CacheProvider._context._currentValue = ReactCache.createCache(); - ReactFetchNode = require('react-fetch'); - fetch = ReactFetchNode.fetch; - } + + fetch = require('react-fetch').fetch; http = require('http'); + waitForSuspense = require('react-suspense-test-utils').waitForSuspense; server = http.createServer((req, res) => { serverImpl(req, res); }); - server.listen(done); - serverEndpoint = `http://localhost:${server.address().port}/`; + serverEndpoint = null; + server.listen(() => { + serverEndpoint = `http://localhost:${server.address().port}/`; + done(); + }); }); afterEach(done => { @@ -41,55 +39,83 @@ describe('ReactFetchNode', () => { server = null; }); - async function waitForSuspense(fn) { - while (true) { - try { - return fn(); - } catch (promise) { - if (typeof promise.then === 'function') { - await promise; - } else { - throw promise; - } - } - } - } + // @gate experimental + it('can fetch text from a server component', async () => { + serverImpl = (req, res) => { + res.write('mango'); + res.end(); + }; + const text = await waitForSuspense(() => { + return fetch(serverEndpoint).text(); + }); + expect(text).toEqual('mango'); + }); // @gate experimental - it('can read text', async () => { + it('can fetch json from a server component', async () => { serverImpl = (req, res) => { - res.write('ok'); + res.write(JSON.stringify({name: 'Sema'})); res.end(); }; - await waitForSuspense(() => { - const response = fetch(serverEndpoint); - expect(response.status).toBe(200); - expect(response.statusText).toBe('OK'); - expect(response.ok).toBe(true); - expect(response.text()).toEqual('ok'); - // Can read again: - expect(response.text()).toEqual('ok'); + const json = await waitForSuspense(() => { + return fetch(serverEndpoint).json(); }); + expect(json).toEqual({name: 'Sema'}); }); // @gate experimental - it('can read json', async () => { + it('provides response status', async () => { serverImpl = (req, res) => { res.write(JSON.stringify({name: 'Sema'})); res.end(); }; - await waitForSuspense(() => { - const response = fetch(serverEndpoint); - expect(response.status).toBe(200); - expect(response.statusText).toBe('OK'); - expect(response.ok).toBe(true); - expect(response.json()).toEqual({ - name: 'Sema', - }); - // Can read again: - expect(response.json()).toEqual({ - name: 'Sema', - }); + const response = await waitForSuspense(() => { + return fetch(serverEndpoint); + }); + expect(response).toMatchObject({ + status: 200, + statusText: 'OK', + ok: true, }); }); + + // @gate experimental + it('handles different paths', async () => { + serverImpl = (req, res) => { + switch (req.url) { + case '/banana': + res.write('banana'); + break; + case '/mango': + res.write('mango'); + break; + case '/orange': + res.write('orange'); + break; + } + res.end(); + }; + const outputs = await waitForSuspense(() => { + return [ + fetch(serverEndpoint + 'banana').text(), + fetch(serverEndpoint + 'mango').text(), + fetch(serverEndpoint + 'orange').text(), + ]; + }); + expect(outputs).toMatchObject(['banana', 'mango', 'orange']); + }); + + // @gate experimental + it('can produce an error', async () => { + serverImpl = (req, res) => {}; + + expect.assertions(1); + try { + await waitForSuspense(() => { + return fetch('BOOM'); + }); + } catch (err) { + expect(err.message).toEqual('Invalid URL: BOOM'); + } + }); }); diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 6f2cb9ca400c1..f257657d2b550 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -25,6 +25,7 @@ import { enableDebugTracing, enableSchedulingProfiler, enableNewReconciler, + enableCache, decoupleUpdatePriorityFromScheduler, enableUseRefAccessWarning, } from 'shared/ReactFeatureFlags'; @@ -1815,6 +1816,10 @@ function dispatchAction( } } +function getCacheForType(resourceType: () => T): T { + invariant(false, 'Not implemented.'); +} + export const ContextOnlyDispatcher: Dispatcher = { readContext, @@ -1835,6 +1840,9 @@ export const ContextOnlyDispatcher: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; +if (enableCache) { + (ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType; +} const HooksDispatcherOnMount: Dispatcher = { readContext, @@ -1877,6 +1885,9 @@ const HooksDispatcherOnUpdate: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; +if (enableCache) { + (HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType; +} const HooksDispatcherOnRerender: Dispatcher = { readContext, @@ -1898,6 +1909,9 @@ const HooksDispatcherOnRerender: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; +if (enableCache) { + (HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType; +} let HooksDispatcherOnMountInDEV: Dispatcher | null = null; let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null; @@ -2052,6 +2066,9 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; + } HooksDispatcherOnMountWithHookTypesInDEV = { readContext( @@ -2174,6 +2191,9 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType; + } HooksDispatcherOnUpdateInDEV = { readContext( @@ -2296,6 +2316,9 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType; + } HooksDispatcherOnRerenderInDEV = { readContext( @@ -2419,6 +2442,9 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType; + } InvalidNestedHooksDispatcherOnMountInDEV = { readContext( @@ -2556,6 +2582,9 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; + } InvalidNestedHooksDispatcherOnUpdateInDEV = { readContext( @@ -2693,6 +2722,9 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType; + } InvalidNestedHooksDispatcherOnRerenderInDEV = { readContext( @@ -2831,4 +2863,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType; + } } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 98e5d709cd285..9fa15c5c0655e 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -25,6 +25,7 @@ import { enableDebugTracing, enableSchedulingProfiler, enableNewReconciler, + enableCache, decoupleUpdatePriorityFromScheduler, enableUseRefAccessWarning, } from 'shared/ReactFeatureFlags'; @@ -1815,6 +1816,10 @@ function dispatchAction( } } +function getCacheForType(resourceType: () => T): T { + invariant(false, 'Not implemented.'); +} + export const ContextOnlyDispatcher: Dispatcher = { readContext, @@ -1835,6 +1840,9 @@ export const ContextOnlyDispatcher: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; +if (enableCache) { + (ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType; +} const HooksDispatcherOnMount: Dispatcher = { readContext, @@ -1856,6 +1864,9 @@ const HooksDispatcherOnMount: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; +if (enableCache) { + (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType; +} const HooksDispatcherOnUpdate: Dispatcher = { readContext, @@ -1877,6 +1888,9 @@ const HooksDispatcherOnUpdate: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; +if (enableCache) { + (HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType; +} const HooksDispatcherOnRerender: Dispatcher = { readContext, @@ -1898,6 +1912,9 @@ const HooksDispatcherOnRerender: Dispatcher = { unstable_isNewReconciler: enableNewReconciler, }; +if (enableCache) { + (HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType; +} let HooksDispatcherOnMountInDEV: Dispatcher | null = null; let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null; @@ -2052,6 +2069,9 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; + } HooksDispatcherOnMountWithHookTypesInDEV = { readContext( @@ -2174,6 +2194,9 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType; + } HooksDispatcherOnUpdateInDEV = { readContext( @@ -2296,6 +2319,9 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType; + } HooksDispatcherOnRerenderInDEV = { readContext( @@ -2419,6 +2445,9 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType; + } InvalidNestedHooksDispatcherOnMountInDEV = { readContext( @@ -2556,6 +2585,9 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; + } InvalidNestedHooksDispatcherOnUpdateInDEV = { readContext( @@ -2693,6 +2725,9 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType; + } InvalidNestedHooksDispatcherOnRerenderInDEV = { readContext( @@ -2831,4 +2866,7 @@ if (__DEV__) { unstable_isNewReconciler: enableNewReconciler, }; + if (enableCache) { + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType; + } } diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index f63495cdce655..ce3f786a5186c 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -274,6 +274,7 @@ type BasicStateAction = (S => S) | S; type Dispatch = A => void; export type Dispatcher = {| + getCacheForType?: (resourceType: () => T) => T, readContext( context: ReactContext, observedBits: void | number | boolean, diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 374ce95397f35..e4f307998ba19 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -74,6 +74,7 @@ type Segment = { export type Request = { destination: Destination, bundlerConfig: BundlerConfig, + cache: Map, nextChunkId: number, pendingChunks: number, pingedSegments: Array, @@ -97,6 +98,7 @@ export function createRequest( const request = { destination, bundlerConfig, + cache: new Map(), nextChunkId: 0, pendingChunks: 0, pingedSegments: pingedSegments, @@ -652,7 +654,9 @@ function retrySegment(request: Request, segment: Segment): void { function performWork(request: Request): void { const prevDispatcher = ReactCurrentDispatcher.current; + const prevCache = currentCache; ReactCurrentDispatcher.current = Dispatcher; + currentCache = request.cache; const pingedSegments = request.pingedSegments; request.pingedSegments = []; @@ -665,6 +669,7 @@ function performWork(request: Request): void { } ReactCurrentDispatcher.current = prevDispatcher; + currentCache = prevCache; } let reentrant = false; @@ -743,6 +748,8 @@ function unsupportedHook(): void { invariant(false, 'This Hook is not supported in Server Components.'); } +let currentCache: Map | null = null; + const Dispatcher: DispatcherType = { useMemo(nextCreate: () => T): T { return nextCreate(); @@ -757,6 +764,19 @@ const Dispatcher: DispatcherType = { useTransition(): [(callback: () => void) => void, boolean] { return [() => {}, false]; }, + getCacheForType(resourceType: () => T): T { + invariant( + currentCache, + 'Reading the cache is only supported while rendering.', + ); + let entry: T | void = (currentCache.get(resourceType): any); + if (entry === undefined) { + entry = resourceType(); + // TODO: Warn if undefined? + currentCache.set(resourceType, entry); + } + return entry; + }, readContext: (unsupportedHook: any), useContext: (unsupportedHook: any), useReducer: (unsupportedHook: any), diff --git a/packages/react-suspense-test-utils/README.md b/packages/react-suspense-test-utils/README.md new file mode 100644 index 0000000000000..77630cd7507bd --- /dev/null +++ b/packages/react-suspense-test-utils/README.md @@ -0,0 +1,12 @@ +# react-suspense-test-utils + +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. + +**Do not use in a real application.** We're publishing this early for +demonstration purposes. + +**Use it at your own risk.** + +# No, Really, It Is Unstable + +The API ~~may~~ will change wildly between versions. diff --git a/packages/react/unstable-cache.js b/packages/react-suspense-test-utils/index.js similarity index 72% rename from packages/react/unstable-cache.js rename to packages/react-suspense-test-utils/index.js index a6b90aa6728a5..d0e5204abab23 100644 --- a/packages/react/unstable-cache.js +++ b/packages/react-suspense-test-utils/index.js @@ -6,4 +6,5 @@ * * @flow */ -export {createCache, readCache, CacheProvider} from './src/cache/ReactCache'; + +export * from './src/ReactSuspenseTestUtils'; diff --git a/packages/react-suspense-test-utils/npm/index.js b/packages/react-suspense-test-utils/npm/index.js new file mode 100644 index 0000000000000..dc9c621a5b77b --- /dev/null +++ b/packages/react-suspense-test-utils/npm/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./cjs/react-suspense-test-utils.js'); diff --git a/packages/react-suspense-test-utils/package.json b/packages/react-suspense-test-utils/package.json new file mode 100644 index 0000000000000..b23c3fc2e31c9 --- /dev/null +++ b/packages/react-suspense-test-utils/package.json @@ -0,0 +1,20 @@ +{ + "name": "react-suspense-test-utils", + "version": "0.1.0", + "private": true, + "repository": { + "type" : "git", + "url" : "https://github.com/facebook/react.git", + "directory": "packages/react-suspense-test-utils" + }, + "license": "MIT", + "files": [ + "LICENSE", + "README.md", + "index.js", + "cjs/" + ], + "peerDependencies": { + "react": "^17.0.0" + } +} diff --git a/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js new file mode 100644 index 0000000000000..a58c6cae824c0 --- /dev/null +++ b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import invariant from 'shared/invariant'; + +const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; + +function unsupported() { + invariant(false, 'This feature is not supported by ReactSuspenseTestUtils.'); +} + +export function waitForSuspense(fn: () => T): Promise { + const cache: Map = new Map(); + const testDispatcher: Dispatcher = { + getCacheForType(resourceType: () => R): R { + let entry: R | void = (cache.get(resourceType): any); + if (entry === undefined) { + entry = resourceType(); + // TODO: Warn if undefined? + cache.set(resourceType, entry); + } + return entry; + }, + readContext: unsupported, + useContext: unsupported, + useMemo: unsupported, + useReducer: unsupported, + useRef: unsupported, + useState: unsupported, + useLayoutEffect: unsupported, + useCallback: unsupported, + useImperativeHandle: unsupported, + useEffect: unsupported, + useDebugValue: unsupported, + useDeferredValue: unsupported, + useTransition: unsupported, + useOpaqueIdentifier: unsupported, + useMutableSource: unsupported, + }; + // Not using async/await because we don't compile it. + return new Promise((resolve, reject) => { + function retry() { + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = testDispatcher; + try { + const result = fn(); + resolve(result); + } catch (thrownValue) { + if (typeof thrownValue.then === 'function') { + thrownValue.then(retry, retry); + } else { + reject(thrownValue); + } + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + } + retry(); + }); +} diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index db5d2b2a22331..04723075defa4 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -50,6 +50,7 @@ export { startTransition as unstable_startTransition, SuspenseList, SuspenseList as unstable_SuspenseList, + unstable_getCacheForType, // enableScopeAPI unstable_Scope, unstable_useOpaqueIdentifier, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index 3b2b47f1ee7cf..5de53908ae883 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -45,6 +45,7 @@ export { startTransition as unstable_startTransition, SuspenseList as unstable_SuspenseList, unstable_useOpaqueIdentifier, + unstable_getCacheForType, // enableDebugTracing unstable_DebugTracingMode, } from './src/React'; diff --git a/packages/react/index.js b/packages/react/index.js index 5a170667c0826..1553bdd9e9a89 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -82,4 +82,5 @@ export { unstable_createFundamental, unstable_Scope, unstable_useOpaqueIdentifier, + unstable_getCacheForType, } from './src/React'; diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index 8217fa158d12c..9a3bb4384ca2e 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -49,6 +49,7 @@ export { startTransition as unstable_startTransition, SuspenseList, SuspenseList as unstable_SuspenseList, + unstable_getCacheForType, // enableScopeAPI unstable_Scope, unstable_useOpaqueIdentifier, diff --git a/packages/react/package.json b/packages/react/package.json index 0dd2c35ee6284..87e5a806a21dd 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -17,8 +17,7 @@ "umd/", "jsx-runtime.js", "jsx-dev-runtime.js", - "unstable-index.server.js", - "unstable-cache.js" + "unstable-index.server.js" ], "main": "index.js", "exports": { diff --git a/packages/react/src/React.js b/packages/react/src/React.js index c5e61fb55c67b..aae52b2750db6 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -33,6 +33,7 @@ import {lazy} from './ReactLazy'; import {forwardRef} from './ReactForwardRef'; import {memo} from './ReactMemo'; import { + getCacheForType, useCallback, useContext, useEffect, @@ -110,6 +111,7 @@ export { useDeferredValue, REACT_SUSPENSE_LIST_TYPE as SuspenseList, REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden, + getCacheForType as unstable_getCacheForType, // enableFundamentalAPI createFundamental as unstable_createFundamental, // enableScopeAPI diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index e1e0879c5f9f2..1020efa74cb96 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -36,6 +36,12 @@ function resolveDispatcher() { return dispatcher; } +export function getCacheForType(resourceType: () => T): T { + const dispatcher = resolveDispatcher(); + // $FlowFixMe This is unstable, thus optional + return dispatcher.getCacheForType(resourceType); +} + export function useContext( Context: ReactContext, unstable_observedBits: number | boolean | void, diff --git a/packages/react/src/__tests__/ReactCache-test.js b/packages/react/src/__tests__/ReactCache-test.js deleted file mode 100644 index 3839d1a6b2e83..0000000000000 --- a/packages/react/src/__tests__/ReactCache-test.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @emails react-core - */ - -'use strict'; - -describe('ReactCache', () => { - let ReactCache; - - beforeEach(() => { - if (__EXPERIMENTAL__) { - ReactCache = require('react/unstable-cache'); - } - }); - - // TODO: test something useful. - // @gate experimental - it('exports something', () => { - expect(ReactCache.readCache).not.toBe(undefined); - }); -}); diff --git a/packages/react/src/cache/ReactCache.js b/packages/react/src/cache/ReactCache.js deleted file mode 100644 index d4240e1dca8ad..0000000000000 --- a/packages/react/src/cache/ReactCache.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * @flow - */ - -import type {ReactContext} from 'shared/ReactTypes'; - -import {createContext} from 'react'; -import invariant from 'shared/invariant'; - -type Cache = {| - resources: Map, -|}; - -// TODO: should there be a default cache? -const CacheContext: ReactContext = createContext(null); - -function CacheImpl() { - this.resources = new Map(); - // TODO: cancellation token. -} - -function createCache(): Cache { - // $FlowFixMe - return new CacheImpl(); -} - -function readCache(): Cache { - // TODO: this doesn't subscribe. - // But we really want load context anyway. - const value = CacheContext._currentValue; - if (value instanceof CacheImpl) { - return value; - } - invariant(false, 'Could not read the cache.'); -} - -const CacheProvider = CacheContext.Provider; - -export {createCache, readCache, CacheProvider}; diff --git a/packages/react/unstable-index.server.experimental.js b/packages/react/unstable-index.server.experimental.js index 479094f46de6f..890066957e383 100644 --- a/packages/react/unstable-index.server.experimental.js +++ b/packages/react/unstable-index.server.experimental.js @@ -32,6 +32,7 @@ export { useDeferredValue as unstable_useDeferredValue, SuspenseList as unstable_SuspenseList, unstable_useOpaqueIdentifier, + unstable_getCacheForType, // enableDebugTracing unstable_DebugTracingMode, } from './src/React'; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index e7f6a22cdf3ed..519ea5015d7d8 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -52,6 +52,7 @@ export const enableSelectiveHydration = __EXPERIMENTAL__; // Flight experiments export const enableLazyElements = __EXPERIMENTAL__; +export const enableCache = __EXPERIMENTAL__; // Only used in www builds. export const enableSchedulerDebugging = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 3c589f1aa687c..32b1e3d45d0a6 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -21,6 +21,7 @@ export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; export const enableLazyElements = false; +export const enableCache = false; export const enableSchedulerDebugging = false; export const debugRenderPhaseSideEffectsForStrictMode = true; export const disableJavaScriptURLs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 2a5da4d2e496b..701cfd745fe36 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -23,6 +23,7 @@ export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; export const enableLazyElements = false; +export const enableCache = false; export const disableJavaScriptURLs = false; export const disableInputAttributeSyncing = false; export const enableSchedulerDebugging = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index dca5277ca1d7b..fb16a242199d6 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -23,6 +23,7 @@ export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; export const enableLazyElements = false; +export const enableCache = false; export const disableJavaScriptURLs = false; export const disableInputAttributeSyncing = false; export const enableSchedulerDebugging = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 49b020333e244..c9e64734268a6 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -23,6 +23,7 @@ export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; export const enableLazyElements = false; +export const enableCache = false; export const disableJavaScriptURLs = false; export const disableInputAttributeSyncing = false; export const enableSchedulerDebugging = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 9c6cec531b585..7002ed80ef6b9 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -23,6 +23,7 @@ export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; export const enableLazyElements = false; +export const enableCache = false; export const enableSchedulerDebugging = false; export const disableJavaScriptURLs = false; export const disableInputAttributeSyncing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index ef61a51dbf9e8..1d028ee23ca40 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -23,6 +23,7 @@ export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; export const enableLazyElements = false; +export const enableCache = false; export const disableJavaScriptURLs = false; export const disableInputAttributeSyncing = false; export const enableSchedulerDebugging = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 42c91d88de1f5..ed3c033e99fff 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -23,6 +23,7 @@ export const enableSchedulerTracing = false; export const enableSuspenseServerRenderer = true; export const enableSelectiveHydration = true; export const enableLazyElements = false; +export const enableCache = false; export const disableJavaScriptURLs = true; export const disableInputAttributeSyncing = false; export const enableSchedulerDebugging = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index d9ae9c183e902..8db205e184369 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -59,6 +59,7 @@ export const enableSuspenseServerRenderer = true; export const enableSelectiveHydration = true; export const enableLazyElements = true; +export const enableCache = true; export const disableJavaScriptURLs = true; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 269b7d2787727..444e3ff809229 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -367,5 +367,7 @@ "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", "377": "BigInt (%s) is not yet supported in client component props. Remove %s from this object or use a plain number instead: %s", "378": "Type %s is not supported in client component props. Remove %s from this object, or avoid the entire object: %s", - "379": "Refs cannot be used in server components, nor passed to client components." + "379": "Refs cannot be used in server components, nor passed to client components.", + "380": "Reading the cache is only supported while rendering.", + "381": "This feature is not supported by ReactSuspenseTestUtils." } diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 6f0f88e04e00b..db8a8b8002cfb 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -135,15 +135,6 @@ const bundles = [ externals: ['react'], }, - /******* React Cache (experimental, new) *******/ - { - bundleTypes: __EXPERIMENTAL__ ? [NODE_DEV, NODE_PROD, NODE_PROFILING] : [], - moduleType: ISOMORPHIC, - entry: 'react/unstable-cache', - global: 'ReactCache', - externals: ['react'], - }, - /******* React Fetch Browser (experimental, new) *******/ { bundleTypes: [NODE_DEV, NODE_PROD], @@ -366,6 +357,15 @@ const bundles = [ ], }, + /******* React Suspense Test Utils *******/ + { + bundleTypes: [NODE_ES2015], + moduleType: RENDERER_UTILS, + entry: 'react-suspense-test-utils', + global: 'ReactSuspenseTestUtils', + externals: ['react'], + }, + /******* React ART *******/ { bundleTypes: [