Skip to content

Commit 0a4c7c5

Browse files
authored
[Flight] Don't warn for key, but error for ref (#19986)
* Improve error message by expanding the object in question * Don't warn for key/ref getters * Error if refs are passed in server components or to client components
1 parent 993ca53 commit 0a4c7c5

File tree

3 files changed

+71
-12
lines changed

3 files changed

+71
-12
lines changed

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

+16
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,15 @@ describe('ReactFlight', () => {
165165
return <div foo={Symbol('foo')} />;
166166
}
167167

168+
const ref = React.createRef();
169+
function RefProp() {
170+
return <div ref={ref} />;
171+
}
172+
168173
const event = ReactNoopFlightServer.render(<EventHandlerProp />);
169174
const fn = ReactNoopFlightServer.render(<FunctionProp />);
170175
const symbol = ReactNoopFlightServer.render(<SymbolProp />);
176+
const refs = ReactNoopFlightServer.render(<RefProp />);
171177

172178
function Client({transport}) {
173179
return ReactNoopFlightClient.read(transport);
@@ -185,6 +191,9 @@ describe('ReactFlight', () => {
185191
<ErrorBoundary expectedMessage="Symbol values (foo) cannot be passed to client components.">
186192
<Client transport={symbol} />
187193
</ErrorBoundary>
194+
<ErrorBoundary expectedMessage="Refs cannot be used in server components, nor passed to client components.">
195+
<Client transport={refs} />
196+
</ErrorBoundary>
188197
</>,
189198
);
190199
});
@@ -217,6 +226,13 @@ describe('ReactFlight', () => {
217226
);
218227
});
219228

229+
it('should NOT warn in DEV for key getters', () => {
230+
const transport = ReactNoopFlightServer.render(<div key="a" />);
231+
act(() => {
232+
ReactNoop.render(ReactNoopFlightClient.read(transport));
233+
});
234+
});
235+
220236
it('should warn in DEV if an object with symbols is passed to a host component', () => {
221237
expect(() => {
222238
const transport = ReactNoopFlightServer.render(

packages/react-server/src/ReactFlightServer.js

+52-10
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ export function createRequest(
119119
function attemptResolveElement(element: React$Element<any>): ReactModel {
120120
const type = element.type;
121121
const props = element.props;
122+
if (element.ref !== null && element.ref !== undefined) {
123+
// When the ref moves to the regular props object this will implicitly
124+
// throw for functions. We could probably relax it to a DEV warning for other
125+
// cases.
126+
invariant(
127+
false,
128+
'Refs cannot be used in server components, nor passed to client components.',
129+
);
130+
}
122131
if (typeof type === 'function') {
123132
// This is a server-side component.
124133
return type(props);
@@ -153,7 +162,11 @@ function attemptResolveElement(element: React$Element<any>): ReactModel {
153162
}
154163
}
155164
}
156-
invariant(false, 'Unsupported type.');
165+
invariant(
166+
false,
167+
'Unsupported server component type: %s',
168+
describeValueForErrorMessage(type),
169+
);
157170
}
158171

159172
function pingSegment(request: Request, segment: Segment): void {
@@ -218,7 +231,19 @@ function isSimpleObject(object): boolean {
218231
const names = Object.getOwnPropertyNames(object);
219232
for (let i = 0; i < names.length; i++) {
220233
const descriptor = Object.getOwnPropertyDescriptor(object, names[i]);
221-
if (!descriptor || !descriptor.enumerable) {
234+
if (!descriptor) {
235+
return false;
236+
}
237+
if (!descriptor.enumerable) {
238+
if (
239+
(names[i] === 'key' || names[i] === 'ref') &&
240+
typeof descriptor.get === 'function'
241+
) {
242+
// React adds key and ref getters to props objects to issue warnings.
243+
// Those getters will not be transferred to the client, but that's ok,
244+
// so we'll special case them.
245+
continue;
246+
}
222247
return false;
223248
}
224249
}
@@ -249,7 +274,7 @@ function describeValueForErrorMessage(value: ReactModel): string {
249274
return '[...]';
250275
}
251276
const name = objectName(value);
252-
if (name === '[object Object]') {
277+
if (name === 'Object') {
253278
return '{...}';
254279
}
255280
return name;
@@ -266,6 +291,7 @@ function describeObjectForErrorMessage(
266291
objectOrArray:
267292
| {+[key: string | number]: ReactModel}
268293
| $ReadOnlyArray<ReactModel>,
294+
expandedName?: string,
269295
): string {
270296
if (isArray(objectOrArray)) {
271297
let str = '[';
@@ -279,7 +305,16 @@ function describeObjectForErrorMessage(
279305
str += '...';
280306
break;
281307
}
282-
str += describeValueForErrorMessage(array[i]);
308+
const value = array[i];
309+
if (
310+
'' + i === expandedName &&
311+
typeof value === 'object' &&
312+
value !== null
313+
) {
314+
str += describeObjectForErrorMessage(value);
315+
} else {
316+
str += describeValueForErrorMessage(value);
317+
}
283318
}
284319
str += ']';
285320
return str;
@@ -297,10 +332,17 @@ function describeObjectForErrorMessage(
297332
break;
298333
}
299334
const name = names[i];
300-
str +=
301-
describeKeyForErrorMessage(name) +
302-
': ' +
303-
describeValueForErrorMessage(object[name]);
335+
str += describeKeyForErrorMessage(name) + ': ';
336+
const value = object[name];
337+
if (
338+
name === expandedName &&
339+
typeof value === 'object' &&
340+
value !== null
341+
) {
342+
str += describeObjectForErrorMessage(value);
343+
} else {
344+
str += describeValueForErrorMessage(value);
345+
}
304346
}
305347
str += '}';
306348
return str;
@@ -444,7 +486,7 @@ export function resolveModelToJSON(
444486
'Classes or other objects with methods are not supported. ' +
445487
'Remove %s from these props: %s',
446488
describeKeyForErrorMessage(key),
447-
describeObjectForErrorMessage(parent),
489+
describeObjectForErrorMessage(parent, key),
448490
);
449491
} else if (Object.getOwnPropertySymbols) {
450492
const symbols = Object.getOwnPropertySymbols(value);
@@ -455,7 +497,7 @@ export function resolveModelToJSON(
455497
'Remove %s from these props: %s',
456498
symbols[0].description,
457499
describeKeyForErrorMessage(key),
458-
describeObjectForErrorMessage(parent),
500+
describeObjectForErrorMessage(parent, key),
459501
);
460502
}
461503
}

scripts/error-codes/codes.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@
340340
"348": "ensureListeningTo(): received a container that was not an element node. This is likely a bug in React.",
341341
"349": "Expected a work-in-progress root. This is a bug in React. Please file an issue.",
342342
"350": "Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.",
343-
"351": "Unsupported type.",
343+
"351": "Unsupported server component type: %s",
344344
"352": "React Blocks (and Lazy Components) are expected to be replaced by a compiler on the server. Try configuring your compiler set up and avoid using React.lazy inside of Blocks.",
345345
"353": "A server block should never encode any other slots. This is a bug in React.",
346346
"354": "getInspectorDataForViewAtPoint() is not available in production.",
@@ -366,5 +366,6 @@
366366
"375": "Functions cannot be passed directly to client components because they're not serializable. Remove %s (%s) from this object, or avoid the entire object: %s",
367367
"376": "Symbol values (%s) cannot be passed to client components. Remove %s from this object, or avoid the entire object: %s",
368368
"377": "BigInt (%s) is not yet supported in client component props. Remove %s from this object or use a plain number instead: %s",
369-
"378": "Type %s is not supported in client component props. Remove %s from this object, or avoid the entire object: %s"
369+
"378": "Type %s is not supported in client component props. Remove %s from this object, or avoid the entire object: %s",
370+
"379": "Refs cannot be used in server components, nor passed to client components."
370371
}

0 commit comments

Comments
 (0)