diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js
index 857ce99868d9b..5e40d0d054225 100644
--- a/packages/react-client/src/__tests__/ReactFlight-test.js
+++ b/packages/react-client/src/__tests__/ReactFlight-test.js
@@ -3423,4 +3423,96 @@ describe('ReactFlight', () => {
);
expect(caughtError.digest).toBe('digest("my-error")');
});
+
+ // @gate __DEV__
+ it('can render deep but cut off JSX in debug info', async () => {
+ function createDeepJSX(n) {
+ if (n <= 0) {
+ return null;
+ }
+ return
{createDeepJSX(n - 1)}
;
+ }
+
+ function ServerComponent(props) {
+ return not using props
;
+ }
+
+ const transport = ReactNoopFlightServer.render({
+ root: (
+
+ {createDeepJSX(100) /* deper than objectLimit */}
+
+ ),
+ });
+
+ await act(async () => {
+ const rootModel = await ReactNoopFlightClient.read(transport);
+ const root = rootModel.root;
+ const children = root._debugInfo[0].props.children;
+ expect(children.type).toBe('div');
+ expect(children.props.children.type).toBe('div');
+ ReactNoop.render(root);
+ });
+
+ expect(ReactNoop).toMatchRenderedOutput(not using props
);
+ });
+
+ // @gate __DEV__
+ it('can render deep but cut off Map/Set in debug info', async () => {
+ function createDeepMap(n) {
+ if (n <= 0) {
+ return null;
+ }
+ const map = new Map();
+ map.set('key', createDeepMap(n - 1));
+ return map;
+ }
+
+ function createDeepSet(n) {
+ if (n <= 0) {
+ return null;
+ }
+ const set = new Set();
+ set.add(createDeepSet(n - 1));
+ return set;
+ }
+
+ function ServerComponent(props) {
+ return not using props
;
+ }
+
+ const transport = ReactNoopFlightServer.render({
+ set: (
+
+ ),
+ map: (
+
+ ),
+ });
+
+ await act(async () => {
+ const rootModel = await ReactNoopFlightClient.read(transport);
+ const set = rootModel.set._debugInfo[0].props.set;
+ const map = rootModel.map._debugInfo[0].props.map;
+ expect(set instanceof Set).toBe(true);
+ expect(set.size).toBe(1);
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
+ for (const entry of set) {
+ expect(entry instanceof Set).toBe(true);
+ break;
+ }
+
+ expect(map instanceof Map).toBe(true);
+ expect(map.size).toBe(1);
+ expect(map.get('key') instanceof Map).toBe(true);
+
+ ReactNoop.render(rootModel.set);
+ });
+
+ expect(ReactNoop).toMatchRenderedOutput(not using props
);
+ });
});
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index 5db03d628146f..de2920d1343ba 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -137,6 +137,9 @@ import binaryToComparableString from 'shared/binaryToComparableString';
import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
+// DEV-only set containing internal objects that should not be limited and turned into getters.
+const doNotLimit: WeakSet = __DEV__ ? new WeakSet() : (null: any);
+
function defaultFilterStackFrame(
filename: string,
functionName: string,
@@ -2147,6 +2150,22 @@ function serializeConsoleMap(
): string {
// Like serializeMap but for renderConsoleValue.
const entries = Array.from(map);
+ // The Map itself doesn't take up any space but the outlined object does.
+ counter.objectLimit++;
+ for (let i = 0; i < entries.length; i++) {
+ // Outline every object entry in case we run out of space to serialize them.
+ // Because we can't mark these values as limited.
+ const entry = entries[i];
+ doNotLimit.add(entry);
+ const key = entry[0];
+ const value = entry[1];
+ if (typeof key === 'object' && key !== null) {
+ doNotLimit.add(key);
+ }
+ if (typeof value === 'object' && value !== null) {
+ doNotLimit.add(value);
+ }
+ }
const id = outlineConsoleValue(request, counter, entries);
return '$Q' + id.toString(16);
}
@@ -2158,6 +2177,16 @@ function serializeConsoleSet(
): string {
// Like serializeMap but for renderConsoleValue.
const entries = Array.from(set);
+ // The Set itself doesn't take up any space but the outlined object does.
+ counter.objectLimit++;
+ for (let i = 0; i < entries.length; i++) {
+ // Outline every object entry in case we run out of space to serialize them.
+ // Because we can't mark these values as limited.
+ const entry = entries[i];
+ if (typeof entry === 'object' && entry !== null) {
+ doNotLimit.add(entry);
+ }
+ }
const id = outlineConsoleValue(request, counter, entries);
return '$W' + id.toString(16);
}
@@ -3362,20 +3391,15 @@ function renderConsoleValue(
parentPropertyName: string,
value: ReactClientValue,
): ReactJSONValue {
- // Make sure that `parent[parentPropertyName]` wasn't JSONified before `value` was passed to us
- // $FlowFixMe[incompatible-use]
- const originalValue = parent[parentPropertyName];
- if (
- typeof originalValue === 'object' &&
- originalValue !== value &&
- !(originalValue instanceof Date)
- ) {
- }
-
if (value === null) {
return null;
}
+ // Special Symbol, that's very common.
+ if (value === REACT_ELEMENT_TYPE) {
+ return '$';
+ }
+
if (typeof value === 'object') {
if (isClientReference(value)) {
// We actually have this value on the client so we could import it.
@@ -3407,7 +3431,7 @@ function renderConsoleValue(
return existingReference;
}
- if (counter.objectLimit <= 0) {
+ if (counter.objectLimit <= 0 && !doNotLimit.has(value)) {
// We've reached our max number of objects to serialize across the wire so we serialize this
// as a marker so that the client can error when this is accessed by the console.
return serializeLimitedObject();
@@ -3427,13 +3451,12 @@ function renderConsoleValue(
if (element._debugStack != null) {
// Outline the debug stack so that it doesn't get cut off.
debugStack = filterStackTrace(request, element._debugStack, 1);
- const stackId = outlineConsoleValue(
- request,
- {objectLimit: debugStack.length + 2},
- debugStack,
- );
- request.writtenObjects.set(debugStack, serializeByValueID(stackId));
+ doNotLimit.add(debugStack);
+ for (let i = 0; i < debugStack.length; i++) {
+ doNotLimit.add(debugStack[i]);
+ }
}
+ doNotLimit.add(element.props);
return [
REACT_ELEMENT_TYPE,
element.type,
@@ -3578,6 +3601,9 @@ function renderConsoleValue(
if (typeof value === 'string') {
if (value[value.length - 1] === 'Z') {
// Possibly a Date, whose toJSON automatically calls toISOString
+ // Make sure that `parent[parentPropertyName]` wasn't JSONified before `value` was passed to us
+ // $FlowFixMe[incompatible-use]
+ const originalValue = parent[parentPropertyName];
if (originalValue instanceof Date) {
return serializeDateFromDateJSON(value);
}
@@ -3662,6 +3688,11 @@ function outlineConsoleValue(
);
}
+ if (typeof model === 'object' && model !== null) {
+ // We can't limit outlined values.
+ doNotLimit.add(model);
+ }
+
function replacer(
this:
| {+[key: string | number]: ReactClientValue}