Skip to content

Commit 566b0b0

Browse files
authored
[Flight] Don't limit objects that are children of special types (#31160)
We can't make a special getter to mark the boundary of deep serialization (which can be used for lazy loading in the future) when the parent object is a special object that we parse with getOutlinedModel. Such as Map/Set and JSX. This marks the objects that are direct children of those as not possible to limit. I don't love this solution since ideally it would maybe be more local to the serialization of a specific object. It also means that very deep trees of only Map/Set never get cut off. Maybe we should instead override the `get()` and enumeration methods on these instead somehow. It's important to have it be a getter though because that's the mechanism that lets us lazy-load more depth in the future.
1 parent 131ae81 commit 566b0b0

File tree

2 files changed

+140
-17
lines changed

2 files changed

+140
-17
lines changed

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3435,4 +3435,96 @@ describe('ReactFlight', () => {
34353435
);
34363436
expect(caughtError.digest).toBe('digest("my-error")');
34373437
});
3438+
3439+
// @gate __DEV__
3440+
it('can render deep but cut off JSX in debug info', async () => {
3441+
function createDeepJSX(n) {
3442+
if (n <= 0) {
3443+
return null;
3444+
}
3445+
return <div>{createDeepJSX(n - 1)}</div>;
3446+
}
3447+
3448+
function ServerComponent(props) {
3449+
return <div>not using props</div>;
3450+
}
3451+
3452+
const transport = ReactNoopFlightServer.render({
3453+
root: (
3454+
<ServerComponent>
3455+
{createDeepJSX(100) /* deper than objectLimit */}
3456+
</ServerComponent>
3457+
),
3458+
});
3459+
3460+
await act(async () => {
3461+
const rootModel = await ReactNoopFlightClient.read(transport);
3462+
const root = rootModel.root;
3463+
const children = root._debugInfo[0].props.children;
3464+
expect(children.type).toBe('div');
3465+
expect(children.props.children.type).toBe('div');
3466+
ReactNoop.render(root);
3467+
});
3468+
3469+
expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
3470+
});
3471+
3472+
// @gate __DEV__
3473+
it('can render deep but cut off Map/Set in debug info', async () => {
3474+
function createDeepMap(n) {
3475+
if (n <= 0) {
3476+
return null;
3477+
}
3478+
const map = new Map();
3479+
map.set('key', createDeepMap(n - 1));
3480+
return map;
3481+
}
3482+
3483+
function createDeepSet(n) {
3484+
if (n <= 0) {
3485+
return null;
3486+
}
3487+
const set = new Set();
3488+
set.add(createDeepSet(n - 1));
3489+
return set;
3490+
}
3491+
3492+
function ServerComponent(props) {
3493+
return <div>not using props</div>;
3494+
}
3495+
3496+
const transport = ReactNoopFlightServer.render({
3497+
set: (
3498+
<ServerComponent
3499+
set={createDeepSet(100) /* deper than objectLimit */}
3500+
/>
3501+
),
3502+
map: (
3503+
<ServerComponent
3504+
map={createDeepMap(100) /* deper than objectLimit */}
3505+
/>
3506+
),
3507+
});
3508+
3509+
await act(async () => {
3510+
const rootModel = await ReactNoopFlightClient.read(transport);
3511+
const set = rootModel.set._debugInfo[0].props.set;
3512+
const map = rootModel.map._debugInfo[0].props.map;
3513+
expect(set instanceof Set).toBe(true);
3514+
expect(set.size).toBe(1);
3515+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
3516+
for (const entry of set) {
3517+
expect(entry instanceof Set).toBe(true);
3518+
break;
3519+
}
3520+
3521+
expect(map instanceof Map).toBe(true);
3522+
expect(map.size).toBe(1);
3523+
expect(map.get('key') instanceof Map).toBe(true);
3524+
3525+
ReactNoop.render(rootModel.set);
3526+
});
3527+
3528+
expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
3529+
});
34383530
});

packages/react-server/src/ReactFlightServer.js

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ import binaryToComparableString from 'shared/binaryToComparableString';
137137

138138
import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
139139

140+
// DEV-only set containing internal objects that should not be limited and turned into getters.
141+
const doNotLimit: WeakSet<Reference> = __DEV__ ? new WeakSet() : (null: any);
142+
140143
function defaultFilterStackFrame(
141144
filename: string,
142145
functionName: string,
@@ -2153,6 +2156,22 @@ function serializeConsoleMap(
21532156
): string {
21542157
// Like serializeMap but for renderConsoleValue.
21552158
const entries = Array.from(map);
2159+
// The Map itself doesn't take up any space but the outlined object does.
2160+
counter.objectLimit++;
2161+
for (let i = 0; i < entries.length; i++) {
2162+
// Outline every object entry in case we run out of space to serialize them.
2163+
// Because we can't mark these values as limited.
2164+
const entry = entries[i];
2165+
doNotLimit.add(entry);
2166+
const key = entry[0];
2167+
const value = entry[1];
2168+
if (typeof key === 'object' && key !== null) {
2169+
doNotLimit.add(key);
2170+
}
2171+
if (typeof value === 'object' && value !== null) {
2172+
doNotLimit.add(value);
2173+
}
2174+
}
21562175
const id = outlineConsoleValue(request, counter, entries);
21572176
return '$Q' + id.toString(16);
21582177
}
@@ -2164,6 +2183,16 @@ function serializeConsoleSet(
21642183
): string {
21652184
// Like serializeMap but for renderConsoleValue.
21662185
const entries = Array.from(set);
2186+
// The Set itself doesn't take up any space but the outlined object does.
2187+
counter.objectLimit++;
2188+
for (let i = 0; i < entries.length; i++) {
2189+
// Outline every object entry in case we run out of space to serialize them.
2190+
// Because we can't mark these values as limited.
2191+
const entry = entries[i];
2192+
if (typeof entry === 'object' && entry !== null) {
2193+
doNotLimit.add(entry);
2194+
}
2195+
}
21672196
const id = outlineConsoleValue(request, counter, entries);
21682197
return '$W' + id.toString(16);
21692198
}
@@ -3376,20 +3405,15 @@ function renderConsoleValue(
33763405
parentPropertyName: string,
33773406
value: ReactClientValue,
33783407
): ReactJSONValue {
3379-
// Make sure that `parent[parentPropertyName]` wasn't JSONified before `value` was passed to us
3380-
// $FlowFixMe[incompatible-use]
3381-
const originalValue = parent[parentPropertyName];
3382-
if (
3383-
typeof originalValue === 'object' &&
3384-
originalValue !== value &&
3385-
!(originalValue instanceof Date)
3386-
) {
3387-
}
3388-
33893408
if (value === null) {
33903409
return null;
33913410
}
33923411

3412+
// Special Symbol, that's very common.
3413+
if (value === REACT_ELEMENT_TYPE) {
3414+
return '$';
3415+
}
3416+
33933417
if (typeof value === 'object') {
33943418
if (isClientReference(value)) {
33953419
// We actually have this value on the client so we could import it.
@@ -3421,7 +3445,7 @@ function renderConsoleValue(
34213445
return existingReference;
34223446
}
34233447

3424-
if (counter.objectLimit <= 0) {
3448+
if (counter.objectLimit <= 0 && !doNotLimit.has(value)) {
34253449
// We've reached our max number of objects to serialize across the wire so we serialize this
34263450
// as a marker so that the client can error when this is accessed by the console.
34273451
return serializeLimitedObject();
@@ -3441,13 +3465,12 @@ function renderConsoleValue(
34413465
if (element._debugStack != null) {
34423466
// Outline the debug stack so that it doesn't get cut off.
34433467
debugStack = filterStackTrace(request, element._debugStack, 1);
3444-
const stackId = outlineConsoleValue(
3445-
request,
3446-
{objectLimit: debugStack.length + 2},
3447-
debugStack,
3448-
);
3449-
request.writtenObjects.set(debugStack, serializeByValueID(stackId));
3468+
doNotLimit.add(debugStack);
3469+
for (let i = 0; i < debugStack.length; i++) {
3470+
doNotLimit.add(debugStack[i]);
3471+
}
34503472
}
3473+
doNotLimit.add(element.props);
34513474
return [
34523475
REACT_ELEMENT_TYPE,
34533476
element.type,
@@ -3592,6 +3615,9 @@ function renderConsoleValue(
35923615
if (typeof value === 'string') {
35933616
if (value[value.length - 1] === 'Z') {
35943617
// Possibly a Date, whose toJSON automatically calls toISOString
3618+
// Make sure that `parent[parentPropertyName]` wasn't JSONified before `value` was passed to us
3619+
// $FlowFixMe[incompatible-use]
3620+
const originalValue = parent[parentPropertyName];
35953621
if (originalValue instanceof Date) {
35963622
return serializeDateFromDateJSON(value);
35973623
}
@@ -3680,6 +3706,11 @@ function outlineConsoleValue(
36803706
);
36813707
}
36823708

3709+
if (typeof model === 'object' && model !== null) {
3710+
// We can't limit outlined values.
3711+
doNotLimit.add(model);
3712+
}
3713+
36833714
function replacer(
36843715
this:
36853716
| {+[key: string | number]: ReactClientValue}

0 commit comments

Comments
 (0)