Skip to content

Commit

Permalink
Add DEV time warnings to enforce that values are plain objects
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Oct 8, 2020
1 parent 75672d7 commit 14266d3
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 7 deletions.
59 changes: 59 additions & 0 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,63 @@ describe('ReactFlight', () => {
);
});
});

it('should warn in DEV if a toJSON instance is passed to a host component', () => {
expect(() => {
const transport = ReactNoopFlightServer.render(
<input value={new Date()} />,
);
act(() => {
ReactNoop.render(ReactNoopFlightClient.read(transport));
});
}).toErrorDev(
'Only plain objects can be passed to client components from server components. ',
{withoutStack: true},
);
});

it('should warn in DEV if a special object is passed to a host component', () => {
expect(() => {
const transport = ReactNoopFlightServer.render(<input value={Math} />);
act(() => {
ReactNoop.render(ReactNoopFlightClient.read(transport));
});
}).toErrorDev(
'Only plain objects can be passed to client components from server components. ' +
'Built-ins like Math are not supported.',
{withoutStack: true},
);
});

it('should warn in DEV if an object with symbols is passed to a host component', () => {
expect(() => {
const transport = ReactNoopFlightServer.render(
<input value={{[Symbol.iterator]: {}}} />,
);
act(() => {
ReactNoop.render(ReactNoopFlightClient.read(transport));
});
}).toErrorDev(
'Only plain objects can be passed to client components from server components. ' +
'Objects with symbol properties like Symbol.iterator are not supported.',
{withoutStack: true},
);
});

it('should warn in DEV if a class instance is passed to a host component', () => {
class Foo {
method() {}
}
expect(() => {
const transport = ReactNoopFlightServer.render(
<input value={new Foo()} />,
);
act(() => {
ReactNoop.render(ReactNoopFlightClient.read(transport));
});
}).toErrorDev(
'Only plain objects can be passed to client components from server components. ',
{withoutStack: true},
);
});
});
101 changes: 94 additions & 7 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
import * as React from 'react';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import invariant from 'shared/invariant';
import is from 'shared/objectIs';

const isArray = Array.isArray;

Expand Down Expand Up @@ -188,6 +189,50 @@ function escapeStringValue(value: string): string {
}
}

function isObjectPrototype(object): boolean {
if (!object) {
return false;
}
// $FlowFixMe
const ObjectPrototype = Object.prototype;
if (object === ObjectPrototype) {
return true;
}
// It might be an object from a different Realm which is
// still just a plain simple object.
if (Object.getPrototypeOf(object)) {
return false;
}
const names = Object.getOwnPropertyNames(object);
for (let i = 0; i < names.length; i++) {
if (!(names[i] in ObjectPrototype)) {
return false;
}
}
return true;
}

function isSimpleObject(object): boolean {
if (!isObjectPrototype(Object.getPrototypeOf(object))) {
return false;
}
const names = Object.getOwnPropertyNames(object);
for (let i = 0; i < names.length; i++) {
const descriptor = Object.getOwnPropertyDescriptor(object, names[i]);
if (!descriptor || !descriptor.enumerable) {
return false;
}
}
return true;
}

function objectName(object): string {
const name = Object.prototype.toString.call(object);
return name.replace(/^\[object (.*)\]$/, function(m, p0) {
return p0;
});
}

function describeKeyForErrorMessage(key: string): string {
const encodedKey = JSON.stringify(key);
return '"' + key + '"' === encodedKey ? key : encodedKey;
Expand All @@ -204,13 +249,10 @@ function describeValueForErrorMessage(value: ReactModel): string {
if (isArray(value)) {
return '[...]';
}
let name = Object.prototype.toString.call(value);
const name = objectName(value);
if (name === '[object Object]') {
return '{...}';
}
name = name.replace(/^\[object (.*)\]$/, function(m, p0) {
return p0;
});
return name;
}
case 'function':
Expand Down Expand Up @@ -246,7 +288,7 @@ function describeObjectForErrorMessage(
let str = '{';
// $FlowFixMe: Should be refined by now.
const object: {+[key: string | number]: ReactModel} = objectOrArray;
const names = Object.getOwnPropertyNames(object);
const names = Object.keys(object);
for (let i = 0; i < names.length; i++) {
if (i > 0) {
str += ', ';
Expand All @@ -272,6 +314,21 @@ export function resolveModelToJSON(
key: string,
value: ReactModel,
): ReactJSONValue {
if (__DEV__) {
// $FlowFixMe
const originalValue = parent[key];
if (!is(originalValue, value)) {
console.error(
'Only plain objects can be passed to client components from server components. ' +
'Objects with toJSON methods are not supported. Convert it manually ' +
'to a simple value before passing it to props. ' +
'Remove %s from these props: %s',
describeKeyForErrorMessage(key),
describeObjectForErrorMessage(parent),
);
}
}

// Special Symbols
switch (value) {
case REACT_ELEMENT_TYPE:
Expand Down Expand Up @@ -371,8 +428,38 @@ export function resolveModelToJSON(

if (typeof value === 'object') {
if (__DEV__) {
if (value !== null) {
return value;
if (value !== null && !isArray(value)) {
// Verify that this is a simple plain object.
if (objectName(value) !== 'Object') {
console.error(
'Only plain objects can be passed to client components from server components. ' +
'Built-ins like %s are not supported. ' +
'Remove %s from these props: %s',
objectName(value),
describeKeyForErrorMessage(key),
describeObjectForErrorMessage(parent),
);
} else if (!isSimpleObject(value)) {
console.error(
'Only plain objects can be passed to client components from server components. ' +
'Classes or other objects with methods are not supported. ' +
'Remove %s from these props: %s',
describeKeyForErrorMessage(key),
describeObjectForErrorMessage(parent),
);
} else if (Object.getOwnPropertySymbols) {
const symbols = Object.getOwnPropertySymbols(value);
if (symbols.length > 0) {
console.error(
'Only plain objects can be passed to client components from server components. ' +
'Objects with symbol properties like %s are not supported. ' +
'Remove %s from these props: %s',
symbols[0].description,
describeKeyForErrorMessage(key),
describeObjectForErrorMessage(parent),
);
}
}
}
}
return value;
Expand Down

0 comments on commit 14266d3

Please sign in to comment.