diff --git a/packages/react-markup/src/__tests__/ReactMarkupAndFlight-test.js b/packages/react-markup/src/__tests__/ReactMarkupAndFlight-test.js
new file mode 100644
index 0000000000000..8e3dd20974d02
--- /dev/null
+++ b/packages/react-markup/src/__tests__/ReactMarkupAndFlight-test.js
@@ -0,0 +1,319 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and 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
+ * @jest-environment node
+ */
+
+'use strict';
+
+if (typeof Blob === 'undefined') {
+ global.Blob = require('buffer').Blob;
+}
+if (typeof File === 'undefined' || typeof FormData === 'undefined') {
+ global.File = require('undici').File;
+ global.FormData = require('undici').FormData;
+}
+
+let act;
+let React;
+let ReactServer;
+let ReactMarkup;
+let ReactNoop;
+let ReactNoopFlightServer;
+let ReactNoopFlightClient;
+
+function normalizeCodeLocInfo(str) {
+ return (
+ str &&
+ String(str).replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
+ return '\n in ' + name + ' (at **)';
+ })
+ );
+}
+
+if (!__EXPERIMENTAL__) {
+ it('should not be built in stable', () => {
+ try {
+ require('react-markup');
+ } catch (x) {
+ return;
+ }
+ throw new Error('Expected react-markup not to exist in stable.');
+ });
+} else {
+ describe('ReactMarkupAndFlight', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ jest.mock('react', () => require('react/react.react-server'));
+ ReactServer = require('react');
+ ReactNoopFlightServer = require('react-noop-renderer/flight-server');
+ // This stores the state so we need to preserve it
+ const flightModules = require('react-noop-renderer/flight-modules');
+ if (__EXPERIMENTAL__) {
+ jest.resetModules();
+ jest.mock('react', () => ReactServer);
+ jest.mock('react-markup', () =>
+ require('react-markup/react-markup.react-server'),
+ );
+ ReactMarkup = require('react-markup');
+ }
+ jest.resetModules();
+ __unmockReact();
+ jest.mock('react-noop-renderer/flight-modules', () => flightModules);
+ React = require('react');
+ ReactNoop = require('react-noop-renderer');
+ ReactNoopFlightClient = require('react-noop-renderer/flight-client');
+ act = require('internal-test-utils').act;
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('supports using react-markup', async () => {
+ async function Preview() {
+ const html =
+ await ReactMarkup.experimental_renderToHTML('Hello, Dave!');
+
+ return
{html}
;
+ }
+
+ const model = ;
+ const transport = ReactNoopFlightServer.render(model);
+
+ await act(async () => {
+ // So it throws here with "Cannot read properties of null (reading 'length')"
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
+ });
+
+ expect(ReactNoop).toMatchRenderedOutput(Hello, Dave!
);
+ });
+
+ it('has a cache if the first renderer is used standalone', async () => {
+ let n = 0;
+ const uncachedFunction = jest.fn(() => {
+ return n++;
+ });
+ const random = ReactServer.cache(uncachedFunction);
+
+ function Random() {
+ return random();
+ }
+
+ function App() {
+ return (
+ <>
+
+ RSC A_1:
+
+
+ RSC A_2:
+
+ >
+ );
+ }
+
+ const model = ;
+ const transport = ReactNoopFlightServer.render(model);
+
+ await act(async () => {
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
+ });
+
+ expect(ReactNoop).toMatchRenderedOutput(
+ <>
+ RSC A_1: 0
+ RSC A_2: 0
+ >,
+ );
+ expect(uncachedFunction).toHaveBeenCalledTimes(1);
+ });
+
+ it('has a cache if the second renderer is used standalone', async () => {
+ let n = 0;
+ const uncachedFunction = jest.fn(() => {
+ return n++;
+ });
+ const random = ReactServer.cache(uncachedFunction);
+
+ function Random() {
+ return random();
+ }
+
+ function App() {
+ return (
+ <>
+
+ RSC B_1:
+
+
+ RSC B_2:
+
+ >
+ );
+ }
+
+ const html = await ReactMarkup.experimental_renderToHTML(
+ ReactServer.createElement(App),
+ );
+
+ expect(html).toEqual('RSC B_1: 0
RSC B_2: 0
');
+ expect(uncachedFunction).toHaveBeenCalledTimes(1);
+ });
+
+ it('shares cache between RSC renderers', async () => {
+ let n = 0;
+ const uncachedFunction = jest.fn(() => {
+ return n++;
+ });
+ const random = ReactServer.cache(uncachedFunction);
+
+ function Random() {
+ return random();
+ }
+
+ async function Preview() {
+ const html = await ReactMarkup.experimental_renderToHTML();
+
+ return (
+ <>
+
+ RSC A:
+
+ RSC B: {html}
+ >
+ );
+ }
+
+ function App() {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ const model = ;
+ const transport = ReactNoopFlightServer.render(model);
+
+ await act(async () => {
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
+ });
+
+ expect(ReactNoop).toMatchRenderedOutput(
+ <>
+ RSC A: 0
+ RSC B: 0
+ RSC A: 0
+ RSC B: 0
+ >,
+ );
+ expect(uncachedFunction).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows correct stacks in nested RSC renderers', async () => {
+ const thrownError = new Error('hi');
+
+ const caughtNestedRendererErrors = [];
+ const ownerStacksDuringParentRendererThrow = [];
+ const ownerStacksDuringNestedRendererThrow = [];
+ function Throw() {
+ if (gate(flags => flags.enableOwnerStacks)) {
+ const stack = ReactServer.captureOwnerStack();
+ ownerStacksDuringNestedRendererThrow.push(
+ normalizeCodeLocInfo(stack),
+ );
+ }
+ throw thrownError;
+ }
+
+ function Indirection() {
+ return ReactServer.createElement(Throw);
+ }
+
+ function App() {
+ return ReactServer.createElement(Indirection);
+ }
+
+ async function Preview() {
+ try {
+ await ReactMarkup.experimental_renderToHTML(
+ ReactServer.createElement(App),
+ {
+ onError: (error, errorInfo) => {
+ caughtNestedRendererErrors.push({
+ error: error,
+ parentStack: errorInfo.componentStack,
+ ownerStack: gate(flags => flags.enableOwnerStacks)
+ ? ReactServer.captureOwnerStack()
+ : null,
+ });
+ },
+ },
+ );
+ } catch (error) {
+ let stack = '';
+ if (gate(flags => flags.enableOwnerStacks)) {
+ stack = ReactServer.captureOwnerStack();
+ ownerStacksDuringParentRendererThrow.push(
+ normalizeCodeLocInfo(stack),
+ );
+ }
+
+ return 'did error';
+ }
+ }
+
+ function PreviewApp() {
+ return ReactServer.createElement(Preview);
+ }
+
+ const model = ReactServer.createElement(PreviewApp);
+ const transport = ReactNoopFlightServer.render(model);
+
+ await act(async () => {
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
+ });
+
+ expect(caughtNestedRendererErrors).toEqual([
+ {
+ error: thrownError,
+ ownerStack:
+ __DEV__ && gate(flags => flags.enableOwnerStacks)
+ ? // TODO: Shouldn't this read the same as the one we got during render?
+ ''
+ : null,
+ // TODO: Shouldn't a parent stack exist?
+ parentStack: undefined,
+ },
+ ]);
+ expect(ownerStacksDuringParentRendererThrow).toEqual(
+ gate(flags => flags.enableOwnerStacks)
+ ? [
+ __DEV__
+ ? // TODO: Should have an owner stack
+ ''
+ : null,
+ ]
+ : [],
+ );
+ expect(ownerStacksDuringNestedRendererThrow).toEqual(
+ gate(flags => flags.enableOwnerStacks)
+ ? [
+ __DEV__
+ ? '\n in Indirection (at **)' +
+ '\n in App (at **)' +
+ '\n in Preview (at **)' +
+ '\n in PreviewApp (at **)'
+ : null,
+ ]
+ : [],
+ );
+ });
+ });
+}
diff --git a/packages/react-reconciler/src/ReactFiberAsyncDispatcher.js b/packages/react-reconciler/src/ReactFiberAsyncDispatcher.js
index d329fb369cae3..9d588ceb49179 100644
--- a/packages/react-reconciler/src/ReactFiberAsyncDispatcher.js
+++ b/packages/react-reconciler/src/ReactFiberAsyncDispatcher.js
@@ -7,7 +7,7 @@
* @flow
*/
-import type {AsyncDispatcher, Fiber} from './ReactInternalTypes';
+import type {AsyncCache, AsyncDispatcher, Fiber} from './ReactInternalTypes';
import type {Cache} from './ReactFiberCacheComponent';
import {enableCache} from 'shared/ReactFeatureFlags';
@@ -18,21 +18,18 @@ import {disableStringRefs} from 'shared/ReactFeatureFlags';
import {current as currentOwner} from './ReactCurrentFiber';
-function getCacheForType(resourceType: () => T): T {
+function getActiveCache(): AsyncCache {
if (!enableCache) {
throw new Error('Not implemented.');
}
+
const cache: Cache = readContext(CacheContext);
- let cacheForType: T | void = (cache.data.get(resourceType): any);
- if (cacheForType === undefined) {
- cacheForType = resourceType();
- cache.data.set(resourceType, cacheForType);
- }
- return cacheForType;
+
+ return cache.data;
}
export const DefaultAsyncDispatcher: AsyncDispatcher = ({
- getCacheForType,
+ getActiveCache,
}: any);
if (__DEV__ || !disableStringRefs) {
diff --git a/packages/react-reconciler/src/ReactFiberCacheComponent.js b/packages/react-reconciler/src/ReactFiberCacheComponent.js
index 64269c5785d13..67031db3b242c 100644
--- a/packages/react-reconciler/src/ReactFiberCacheComponent.js
+++ b/packages/react-reconciler/src/ReactFiberCacheComponent.js
@@ -8,7 +8,7 @@
*/
import type {ReactContext} from 'shared/ReactTypes';
-import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
+import type {AsyncCache, Fiber} from 'react-reconciler/src/ReactInternalTypes';
import {enableCache} from 'shared/ReactFeatureFlags';
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
@@ -42,7 +42,7 @@ const AbortControllerLocal: typeof AbortController = enableCache
export type Cache = {
controller: AbortController,
- data: Map<() => mixed, mixed>,
+ data: AsyncCache,
refCount: number,
};
diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js
index 4549253ba79b6..8ed9ee4b126d7 100644
--- a/packages/react-reconciler/src/ReactInternalTypes.js
+++ b/packages/react-reconciler/src/ReactInternalTypes.js
@@ -451,8 +451,13 @@ export type Dispatcher = {
) => [Awaited, (P) => void, boolean],
};
+export interface AsyncCache {
+ get(resourceType: Function): mixed;
+ set(resourceType: Function, value: mixed): AsyncCache;
+}
+
export type AsyncDispatcher = {
- getCacheForType: (resourceType: () => T) => T,
+ getActiveCache: () => AsyncCache | null,
// DEV-only (or !disableStringRefs)
getOwner: () => null | Fiber | ReactComponentInfo | ComponentStackNode,
};
diff --git a/packages/react-server/src/ReactFizzAsyncDispatcher.js b/packages/react-server/src/ReactFizzAsyncDispatcher.js
index 3a548ae138039..d37acf770078e 100644
--- a/packages/react-server/src/ReactFizzAsyncDispatcher.js
+++ b/packages/react-server/src/ReactFizzAsyncDispatcher.js
@@ -7,19 +7,22 @@
* @flow
*/
-import type {AsyncDispatcher} from 'react-reconciler/src/ReactInternalTypes';
+import type {
+ AsyncCache,
+ AsyncDispatcher,
+} from 'react-reconciler/src/ReactInternalTypes';
import type {ComponentStackNode} from './ReactFizzComponentStack';
import {disableStringRefs} from 'shared/ReactFeatureFlags';
import {currentTaskInDEV} from './ReactFizzCurrentTask';
-function getCacheForType(resourceType: () => T): T {
+function getActiveCache(): AsyncCache | null {
throw new Error('Not implemented.');
}
export const DefaultAsyncDispatcher: AsyncDispatcher = ({
- getCacheForType,
+ getActiveCache,
}: any);
if (__DEV__) {
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index df811a8c7fa99..2866c3a59a0be 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -10,10 +10,12 @@
import type {Chunk, BinaryChunk, Destination} from './ReactServerStreamConfig';
import type {Postpone} from 'react/src/ReactPostpone';
+import type {AsyncCache} from 'react-reconciler/src/ReactInternalTypes';
import type {TemporaryReferenceSet} from './ReactFlightServerTemporaryReferences';
import {
+ disableStringRefs,
enableBinaryFlight,
enablePostpone,
enableHalt,
@@ -448,15 +450,24 @@ function RequestInstance(
onAllReady: () => void,
onFatalError: (error: mixed) => void,
) {
- if (
- ReactSharedInternals.A !== null &&
- ReactSharedInternals.A !== DefaultAsyncDispatcher
- ) {
- throw new Error(
- 'Currently React only supports one RSC renderer at a time.',
- );
+ const previousAsyncDispatcher = ReactSharedInternals.A;
+
+ if (previousAsyncDispatcher !== null) {
+ const previousActiveCache = previousAsyncDispatcher.getActiveCache();
+ if (previousActiveCache !== null) {
+ ReactSharedInternals.A = ({
+ getActiveCache(): AsyncCache | null {
+ return previousActiveCache;
+ },
+ }: any);
+ if (__DEV__ || !disableStringRefs) {
+ ReactSharedInternals.A.getOwner = DefaultAsyncDispatcher.getOwner;
+ }
+ }
+ } else {
+ ReactSharedInternals.A = DefaultAsyncDispatcher;
}
- ReactSharedInternals.A = DefaultAsyncDispatcher;
+
if (__DEV__) {
// Unlike Fizz or Fiber, we don't reset this and just keep it on permanently.
// This lets it act more like the AsyncDispatcher so that we can get the
diff --git a/packages/react-server/src/flight/ReactFlightAsyncDispatcher.js b/packages/react-server/src/flight/ReactFlightAsyncDispatcher.js
index f5f031a860ff9..190304cc24b6b 100644
--- a/packages/react-server/src/flight/ReactFlightAsyncDispatcher.js
+++ b/packages/react-server/src/flight/ReactFlightAsyncDispatcher.js
@@ -9,7 +9,10 @@
import type {ReactComponentInfo} from 'shared/ReactTypes';
-import type {AsyncDispatcher} from 'react-reconciler/src/ReactInternalTypes';
+import type {
+ AsyncCache,
+ AsyncDispatcher,
+} from 'react-reconciler/src/ReactInternalTypes';
import {resolveRequest, getCache} from '../ReactFlightServer';
@@ -17,25 +20,16 @@ import {disableStringRefs} from 'shared/ReactFeatureFlags';
import {resolveOwner} from './ReactFlightCurrentOwner';
-function resolveCache(): Map {
+function getActiveCache(): AsyncCache | null {
const request = resolveRequest();
if (request) {
return getCache(request);
}
- return new Map();
+ return null;
}
export const DefaultAsyncDispatcher: AsyncDispatcher = ({
- getCacheForType(resourceType: () => T): T {
- const cache = resolveCache();
- let entry: T | void = (cache.get(resourceType): any);
- if (entry === undefined) {
- entry = resourceType();
- // TODO: Warn if undefined?
- cache.set(resourceType, entry);
- }
- return entry;
- },
+ getActiveCache,
}: any);
if (__DEV__) {
diff --git a/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js
index bc2d4bee79d02..e14623807f5ad 100644
--- a/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js
+++ b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js
@@ -13,14 +13,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
export function waitForSuspense(fn: () => T): Promise {
const cache: Map = new Map();
const testDispatcher: AsyncDispatcher = {
- 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;
+ getActiveCache() {
+ return cache;
},
getOwner(): null {
return null;
diff --git a/packages/react/src/ReactCacheImpl.js b/packages/react/src/ReactCacheImpl.js
index cc3136897e350..e71b5d3c72e47 100644
--- a/packages/react/src/ReactCacheImpl.js
+++ b/packages/react/src/ReactCacheImpl.js
@@ -60,9 +60,18 @@ export function cache, T>(fn: (...A) => T): (...A) => T {
// $FlowFixMe[incompatible-call]: We don't want to use rest arguments since we transpile the code.
return fn.apply(null, arguments);
}
- const fnMap: WeakMap> = dispatcher.getCacheForType(
- createCacheRoot,
- );
+ const activeCache = dispatcher.getActiveCache();
+ let fnMap: WeakMap> | void =
+ activeCache !== null
+ ? (activeCache.get(createCacheRoot): any)
+ : undefined;
+ if (fnMap === undefined) {
+ fnMap = createCacheRoot();
+ if (activeCache !== null) {
+ // TODO: Warn if undefined?
+ activeCache.set(createCacheRoot, fnMap);
+ }
+ }
const fnNode = fnMap.get(fn);
let cacheNode: CacheNode;
if (fnNode === undefined) {
diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js
index 956a2a96b44a1..6543fd95a5ef3 100644
--- a/packages/react/src/ReactHooks.js
+++ b/packages/react/src/ReactHooks.js
@@ -53,7 +53,17 @@ export function getCacheForType(resourceType: () => T): T {
// If there is no dispatcher, then we treat this as not being cached.
return resourceType();
}
- return dispatcher.getCacheForType(resourceType);
+ const activeCache = dispatcher.getActiveCache();
+ let entry: T | void =
+ activeCache !== null ? (activeCache.get(resourceType): any) : undefined;
+ if (entry === undefined) {
+ entry = resourceType();
+ if (activeCache !== null) {
+ // TODO: Warn if undefined?
+ activeCache.set(resourceType, entry);
+ }
+ }
+ return entry;
}
export function useContext(Context: ReactContext): T {