Skip to content

Commit 14266d3

Browse files
committed
Add DEV time warnings to enforce that values are plain objects
1 parent 75672d7 commit 14266d3

File tree

2 files changed

+153
-7
lines changed

2 files changed

+153
-7
lines changed

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,63 @@ describe('ReactFlight', () => {
189189
);
190190
});
191191
});
192+
193+
it('should warn in DEV if a toJSON instance is passed to a host component', () => {
194+
expect(() => {
195+
const transport = ReactNoopFlightServer.render(
196+
<input value={new Date()} />,
197+
);
198+
act(() => {
199+
ReactNoop.render(ReactNoopFlightClient.read(transport));
200+
});
201+
}).toErrorDev(
202+
'Only plain objects can be passed to client components from server components. ',
203+
{withoutStack: true},
204+
);
205+
});
206+
207+
it('should warn in DEV if a special object is passed to a host component', () => {
208+
expect(() => {
209+
const transport = ReactNoopFlightServer.render(<input value={Math} />);
210+
act(() => {
211+
ReactNoop.render(ReactNoopFlightClient.read(transport));
212+
});
213+
}).toErrorDev(
214+
'Only plain objects can be passed to client components from server components. ' +
215+
'Built-ins like Math are not supported.',
216+
{withoutStack: true},
217+
);
218+
});
219+
220+
it('should warn in DEV if an object with symbols is passed to a host component', () => {
221+
expect(() => {
222+
const transport = ReactNoopFlightServer.render(
223+
<input value={{[Symbol.iterator]: {}}} />,
224+
);
225+
act(() => {
226+
ReactNoop.render(ReactNoopFlightClient.read(transport));
227+
});
228+
}).toErrorDev(
229+
'Only plain objects can be passed to client components from server components. ' +
230+
'Objects with symbol properties like Symbol.iterator are not supported.',
231+
{withoutStack: true},
232+
);
233+
});
234+
235+
it('should warn in DEV if a class instance is passed to a host component', () => {
236+
class Foo {
237+
method() {}
238+
}
239+
expect(() => {
240+
const transport = ReactNoopFlightServer.render(
241+
<input value={new Foo()} />,
242+
);
243+
act(() => {
244+
ReactNoop.render(ReactNoopFlightClient.read(transport));
245+
});
246+
}).toErrorDev(
247+
'Only plain objects can be passed to client components from server components. ',
248+
{withoutStack: true},
249+
);
250+
});
192251
});

packages/react-server/src/ReactFlightServer.js

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
import * as React from 'react';
5050
import ReactSharedInternals from 'shared/ReactSharedInternals';
5151
import invariant from 'shared/invariant';
52+
import is from 'shared/objectIs';
5253

5354
const isArray = Array.isArray;
5455

@@ -188,6 +189,50 @@ function escapeStringValue(value: string): string {
188189
}
189190
}
190191

192+
function isObjectPrototype(object): boolean {
193+
if (!object) {
194+
return false;
195+
}
196+
// $FlowFixMe
197+
const ObjectPrototype = Object.prototype;
198+
if (object === ObjectPrototype) {
199+
return true;
200+
}
201+
// It might be an object from a different Realm which is
202+
// still just a plain simple object.
203+
if (Object.getPrototypeOf(object)) {
204+
return false;
205+
}
206+
const names = Object.getOwnPropertyNames(object);
207+
for (let i = 0; i < names.length; i++) {
208+
if (!(names[i] in ObjectPrototype)) {
209+
return false;
210+
}
211+
}
212+
return true;
213+
}
214+
215+
function isSimpleObject(object): boolean {
216+
if (!isObjectPrototype(Object.getPrototypeOf(object))) {
217+
return false;
218+
}
219+
const names = Object.getOwnPropertyNames(object);
220+
for (let i = 0; i < names.length; i++) {
221+
const descriptor = Object.getOwnPropertyDescriptor(object, names[i]);
222+
if (!descriptor || !descriptor.enumerable) {
223+
return false;
224+
}
225+
}
226+
return true;
227+
}
228+
229+
function objectName(object): string {
230+
const name = Object.prototype.toString.call(object);
231+
return name.replace(/^\[object (.*)\]$/, function(m, p0) {
232+
return p0;
233+
});
234+
}
235+
191236
function describeKeyForErrorMessage(key: string): string {
192237
const encodedKey = JSON.stringify(key);
193238
return '"' + key + '"' === encodedKey ? key : encodedKey;
@@ -204,13 +249,10 @@ function describeValueForErrorMessage(value: ReactModel): string {
204249
if (isArray(value)) {
205250
return '[...]';
206251
}
207-
let name = Object.prototype.toString.call(value);
252+
const name = objectName(value);
208253
if (name === '[object Object]') {
209254
return '{...}';
210255
}
211-
name = name.replace(/^\[object (.*)\]$/, function(m, p0) {
212-
return p0;
213-
});
214256
return name;
215257
}
216258
case 'function':
@@ -246,7 +288,7 @@ function describeObjectForErrorMessage(
246288
let str = '{';
247289
// $FlowFixMe: Should be refined by now.
248290
const object: {+[key: string | number]: ReactModel} = objectOrArray;
249-
const names = Object.getOwnPropertyNames(object);
291+
const names = Object.keys(object);
250292
for (let i = 0; i < names.length; i++) {
251293
if (i > 0) {
252294
str += ', ';
@@ -272,6 +314,21 @@ export function resolveModelToJSON(
272314
key: string,
273315
value: ReactModel,
274316
): ReactJSONValue {
317+
if (__DEV__) {
318+
// $FlowFixMe
319+
const originalValue = parent[key];
320+
if (!is(originalValue, value)) {
321+
console.error(
322+
'Only plain objects can be passed to client components from server components. ' +
323+
'Objects with toJSON methods are not supported. Convert it manually ' +
324+
'to a simple value before passing it to props. ' +
325+
'Remove %s from these props: %s',
326+
describeKeyForErrorMessage(key),
327+
describeObjectForErrorMessage(parent),
328+
);
329+
}
330+
}
331+
275332
// Special Symbols
276333
switch (value) {
277334
case REACT_ELEMENT_TYPE:
@@ -371,8 +428,38 @@ export function resolveModelToJSON(
371428

372429
if (typeof value === 'object') {
373430
if (__DEV__) {
374-
if (value !== null) {
375-
return value;
431+
if (value !== null && !isArray(value)) {
432+
// Verify that this is a simple plain object.
433+
if (objectName(value) !== 'Object') {
434+
console.error(
435+
'Only plain objects can be passed to client components from server components. ' +
436+
'Built-ins like %s are not supported. ' +
437+
'Remove %s from these props: %s',
438+
objectName(value),
439+
describeKeyForErrorMessage(key),
440+
describeObjectForErrorMessage(parent),
441+
);
442+
} else if (!isSimpleObject(value)) {
443+
console.error(
444+
'Only plain objects can be passed to client components from server components. ' +
445+
'Classes or other objects with methods are not supported. ' +
446+
'Remove %s from these props: %s',
447+
describeKeyForErrorMessage(key),
448+
describeObjectForErrorMessage(parent),
449+
);
450+
} else if (Object.getOwnPropertySymbols) {
451+
const symbols = Object.getOwnPropertySymbols(value);
452+
if (symbols.length > 0) {
453+
console.error(
454+
'Only plain objects can be passed to client components from server components. ' +
455+
'Objects with symbol properties like %s are not supported. ' +
456+
'Remove %s from these props: %s',
457+
symbols[0].description,
458+
describeKeyForErrorMessage(key),
459+
describeObjectForErrorMessage(parent),
460+
);
461+
}
462+
}
376463
}
377464
}
378465
return value;

0 commit comments

Comments
 (0)