Skip to content

Commit c0fe18d

Browse files
committed
Fix bug where references deep inside client elements couldn't be deduped.
1 parent 346cc39 commit c0fe18d

File tree

2 files changed

+24
-12
lines changed

2 files changed

+24
-12
lines changed

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

+8-7
Original file line numberDiff line numberDiff line change
@@ -224,14 +224,12 @@ describe('ReactFlightDOMEdge', () => {
224224
this: {is: 'a large objected'},
225225
with: {many: 'properties in it'},
226226
};
227-
const props = {
228-
items: new Array(30).fill(obj),
229-
};
227+
const props = {root: <div>{new Array(30).fill(obj)}</div>};
230228
const stream = ReactServerDOMServer.renderToReadableStream(props);
231229
const [stream1, stream2] = passThrough(stream).tee();
232230

233231
const serializedContent = await readResult(stream1);
234-
expect(serializedContent.length).toBeLessThan(470);
232+
expect(serializedContent.length).toBeLessThan(1100);
235233

236234
const result = await ReactServerDOMClient.createFromReadableStream(
237235
stream2,
@@ -242,10 +240,13 @@ describe('ReactFlightDOMEdge', () => {
242240
},
243241
},
244242
);
243+
// TODO: Cyclic references currently cause a Lazy wrapper which is not ideal.
244+
const resultElement = result.root._init(result.root._payload);
245245
// Should still match the result when parsed
246-
expect(result).toEqual(props);
247-
expect(result.items[5]).toBe(result.items[10]); // two random items are the same instance
248-
// TODO: items[0] is not the same as the others in this case
246+
expect(resultElement).toEqual(props.root);
247+
expect(resultElement.props.children[5]).toBe(
248+
resultElement.props.children[10],
249+
); // two random items are the same instance
249250
});
250251

251252
it('should execute repeated server components only once', async () => {

packages/react-server/src/ReactFlightServer.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -2096,6 +2096,7 @@ function renderModelDestructive(
20962096
if (typeof value === 'object') {
20972097
switch ((value: any).$$typeof) {
20982098
case REACT_ELEMENT_TYPE: {
2099+
let elementReference = null;
20992100
const writtenObjects = request.writtenObjects;
21002101
if (task.keyPath !== null || task.implicitSlot) {
21012102
// If we're in some kind of context we can't reuse the result of this render or
@@ -2124,10 +2125,8 @@ function renderModelDestructive(
21242125
if (parentReference !== undefined) {
21252126
// If the parent has a reference, we can refer to this object indirectly
21262127
// through the property name inside that parent.
2127-
writtenObjects.set(
2128-
value,
2129-
parentReference + ':' + parentPropertyName,
2130-
);
2128+
elementReference = parentReference + ':' + parentPropertyName;
2129+
writtenObjects.set(value, elementReference);
21312130
}
21322131
}
21332132
}
@@ -2162,7 +2161,7 @@ function renderModelDestructive(
21622161
}
21632162

21642163
// Attempt to render the Server Component.
2165-
return renderElement(
2164+
const newChild = renderElement(
21662165
request,
21672166
task,
21682167
element.type,
@@ -2178,6 +2177,18 @@ function renderModelDestructive(
21782177
: null,
21792178
__DEV__ && enableOwnerStacks ? element._store.validated : 0,
21802179
);
2180+
if (
2181+
typeof newChild === 'object' &&
2182+
newChild !== null &&
2183+
elementReference !== null
2184+
) {
2185+
// If this element renders another object, we can now refer to that object through
2186+
// the same location as this element.
2187+
if (!writtenObjects.has(newChild)) {
2188+
writtenObjects.set(newChild, elementReference);
2189+
}
2190+
}
2191+
return newChild;
21812192
}
21822193
case REACT_LAZY_TYPE: {
21832194
// Reset the task's thenable state before continuing. If there was one, it was

0 commit comments

Comments
 (0)