Skip to content

Commit 27a18ba

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

File tree

2 files changed

+152
-7
lines changed

2 files changed

+152
-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: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,50 @@ function escapeStringValue(value: string): string {
188188
}
189189
}
190190

191+
function isObjectPrototype(object): boolean {
192+
if (!object) {
193+
return false;
194+
}
195+
// $FlowFixMe
196+
const ObjectPrototype = Object.prototype;
197+
if (object === ObjectPrototype) {
198+
return true;
199+
}
200+
// It might be an object from a different Realm which is
201+
// still just a plain simple object.
202+
if (Object.getPrototypeOf(object)) {
203+
return false;
204+
}
205+
const names = Object.getOwnPropertyNames(object);
206+
for (let i = 0; i < names.length; i++) {
207+
if (!(names[i] in ObjectPrototype)) {
208+
return false;
209+
}
210+
}
211+
return true;
212+
}
213+
214+
function isSimpleObject(object): boolean {
215+
if (!isObjectPrototype(Object.getPrototypeOf(object))) {
216+
return false;
217+
}
218+
const names = Object.getOwnPropertyNames(object);
219+
for (let i = 0; i < names.length; i++) {
220+
const descriptor = Object.getOwnPropertyDescriptor(object, names[i]);
221+
if (!descriptor || !descriptor.enumerable) {
222+
return false;
223+
}
224+
}
225+
return true;
226+
}
227+
228+
function objectName(object): string {
229+
const name = Object.prototype.toString.call(object);
230+
return name.replace(/^\[object (.*)\]$/, function(m, p0) {
231+
return p0;
232+
});
233+
}
234+
191235
function describeKeyForErrorMessage(key: string): string {
192236
const encodedKey = JSON.stringify(key);
193237
return '"' + key + '"' === encodedKey ? key : encodedKey;
@@ -204,13 +248,10 @@ function describeValueForErrorMessage(value: ReactModel): string {
204248
if (isArray(value)) {
205249
return '[...]';
206250
}
207-
let name = Object.prototype.toString.call(value);
251+
const name = objectName(value);
208252
if (name === '[object Object]') {
209253
return '{...}';
210254
}
211-
name = name.replace(/^\[object (.*)\]$/, function(m, p0) {
212-
return p0;
213-
});
214255
return name;
215256
}
216257
case 'function':
@@ -246,7 +287,7 @@ function describeObjectForErrorMessage(
246287
let str = '{';
247288
// $FlowFixMe: Should be refined by now.
248289
const object: {+[key: string | number]: ReactModel} = objectOrArray;
249-
const names = Object.getOwnPropertyNames(object);
290+
const names = Object.keys(object);
250291
for (let i = 0; i < names.length; i++) {
251292
if (i > 0) {
252293
str += ', ';
@@ -272,6 +313,21 @@ export function resolveModelToJSON(
272313
key: string,
273314
value: ReactModel,
274315
): ReactJSONValue {
316+
if (__DEV__) {
317+
// $FlowFixMe
318+
const originalValue = parent[key];
319+
if (typeof originalValue === 'object' && originalValue !== value) {
320+
console.error(
321+
'Only plain objects can be passed to client components from server components. ' +
322+
'Objects with toJSON methods are not supported. Convert it manually ' +
323+
'to a simple value before passing it to props. ' +
324+
'Remove %s from these props: %s %s',
325+
describeKeyForErrorMessage(key),
326+
describeObjectForErrorMessage(parent),
327+
);
328+
}
329+
}
330+
275331
// Special Symbols
276332
switch (value) {
277333
case REACT_ELEMENT_TYPE:
@@ -371,8 +427,38 @@ export function resolveModelToJSON(
371427

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

0 commit comments

Comments
 (0)