From de8901f6d990cae645c6df6d91aa88d5a5a3c1ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Sun, 16 Oct 2022 21:49:17 -0400 Subject: [PATCH] [Flight] Improve Error Messages when Invalid Object is Passed to Client/Host Components (#25492) * Print built-in specific error message for toJSON This is a better message for Date. Also, format the message to highlight the affected prop. * Describe error messages using JSX elements in DEV We don't have access to the grand parent objects on the stack so we stash them on weakmaps so we can access them while printing error messages. Might be a bit slow. * Capitalize Server/Client Component * Special case errror messages for children of host components These are likely meant to be text content if they're not a supported object. * Update error messages --- .../src/__tests__/ReactFlight-test.js | 231 ++++++++++-- .../ReactFlightDOMRelay-test.internal.js | 6 +- .../ReactFlightNativeRelay-test.internal.js | 6 +- ...actFlightNativeRelay-test.internal.js.snap | 4 +- .../react-server/src/ReactFlightServer.js | 334 ++++++++++++------ scripts/error-codes/codes.json | 14 +- 6 files changed, 445 insertions(+), 150 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index b837f3ff8bb2c..69926bee20639 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -95,7 +95,7 @@ describe('ReactFlight', () => { }; } - it('can render a server component', async () => { + it('can render a Server Component', async () => { function Bar({text}) { return text.toUpperCase(); } @@ -125,7 +125,7 @@ describe('ReactFlight', () => { }); }); - it('can render a client component using a module reference and render there', async () => { + it('can render a Client Component using a module reference and render there', async () => { function UserClient(props) { return ( @@ -363,6 +363,11 @@ describe('ReactFlight', () => { // @gate enableUseHook it('should error if a non-serializable value is passed to a host component', async () => { + function ClientImpl({children}) { + return children; + } + const Client = moduleReference(ClientImpl); + function EventHandlerProp() { return (
@@ -382,6 +387,24 @@ describe('ReactFlight', () => { return
; } + function EventHandlerPropClient() { + return ( + + Test + + ); + } + function FunctionPropClient() { + return {() => {}}; + } + function SymbolPropClient() { + return ; + } + + function RefPropClient() { + return ; + } + const options = { onError(x) { return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; @@ -391,8 +414,21 @@ describe('ReactFlight', () => { const fn = ReactNoopFlightServer.render(, options); const symbol = ReactNoopFlightServer.render(, options); const refs = ReactNoopFlightServer.render(, options); + const eventClient = ReactNoopFlightServer.render( + , + options, + ); + const fnClient = ReactNoopFlightServer.render( + , + options, + ); + const symbolClient = ReactNoopFlightServer.render( + , + options, + ); + const refsClient = ReactNoopFlightServer.render(, options); - function Client({promise}) { + function Render({promise}) { return use(promise); } @@ -400,17 +436,29 @@ describe('ReactFlight', () => { startTransition(() => { ReactNoop.render( <> - - + + + + + + + + + + + + + + - - + + - - + + - - + + , ); @@ -419,19 +467,19 @@ describe('ReactFlight', () => { }); // @gate enableUseHook - it('should trigger the inner most error boundary inside a client component', async () => { + it('should trigger the inner most error boundary inside a Client Component', async () => { function ServerComponent() { - throw new Error('This was thrown in the server component.'); + throw new Error('This was thrown in the Server Component.'); } function ClientComponent({children}) { - // This should catch the error thrown by the server component, even though it has already happened. + // This should catch the error thrown by the Server Component, even though it has already happened. // We currently need to wrap it in a div because as it's set up right now, a lazy reference will // throw during reconciliation which will trigger the parent of the error boundary. // This is similar to how these will suspend the parent if it's a direct child of a Suspense boundary. // That's a bug. return ( - +
{children}
); @@ -475,25 +523,37 @@ describe('ReactFlight', () => { ); ReactNoopFlightClient.read(transport); }).toErrorDev( - 'Only plain objects can be passed to client components from server components. ', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Date objects are not supported.', {withoutStack: true}, ); }); - it('should warn in DEV if a special object is passed to a host component', () => { + it('should warn in DEV if a toJSON instance is passed to a host component child', () => { expect(() => { - const transport = ReactNoopFlightServer.render(); + const transport = ReactNoopFlightServer.render( +
Current date: {new Date()}
, + ); ReactNoopFlightClient.read(transport); }).toErrorDev( - 'Only plain objects can be passed to client components from server components. ' + - 'Built-ins like Math are not supported.', + 'Date objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Current date: {Date}
\n' + + ' ^^^^^^', {withoutStack: true}, ); }); - it('should NOT warn in DEV for key getters', () => { - const transport = ReactNoopFlightServer.render(
); - ReactNoopFlightClient.read(transport); + it('should warn in DEV if a special object is passed to a host component', () => { + expect(() => { + const transport = ReactNoopFlightServer.render(); + ReactNoopFlightClient.read(transport); + }).toErrorDev( + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' \n' + + ' ^^^^^^', + {withoutStack: true}, + ); }); it('should warn in DEV if an object with symbols is passed to a host component', () => { @@ -503,12 +563,127 @@ describe('ReactFlight', () => { ); ReactNoopFlightClient.read(transport); }).toErrorDev( - 'Only plain objects can be passed to client components from server components. ' + + '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 toJSON instance is passed to a Client Component', () => { + function ClientImpl({value}) { + return
{value}
; + } + const Client = moduleReference(ClientImpl); + expect(() => { + const transport = ReactNoopFlightServer.render( + , + ); + ReactNoopFlightClient.read(transport); + }).toErrorDev( + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Date objects are not supported.', + {withoutStack: true}, + ); + }); + + it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => { + function ClientImpl({children}) { + return
{children}
; + } + const Client = moduleReference(ClientImpl); + expect(() => { + const transport = ReactNoopFlightServer.render( + Current date: {new Date()}, + ); + ReactNoopFlightClient.read(transport); + }).toErrorDev( + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Date objects are not supported.\n' + + ' <>Current date: {Date}\n' + + ' ^^^^^^', + {withoutStack: true}, + ); + }); + + it('should warn in DEV if a special object is passed to a Client Component', () => { + function ClientImpl({value}) { + return
{value}
; + } + const Client = moduleReference(ClientImpl); + expect(() => { + const transport = ReactNoopFlightServer.render(); + ReactNoopFlightClient.read(transport); + }).toErrorDev( + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' <... value={Math}>\n' + + ' ^^^^^^', + {withoutStack: true}, + ); + }); + + it('should warn in DEV if an object with symbols is passed to a Client Component', () => { + function ClientImpl({value}) { + return
{value}
; + } + const Client = moduleReference(ClientImpl); + expect(() => { + const transport = ReactNoopFlightServer.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 special object is passed to a nested object in Client Component', () => { + function ClientImpl({value}) { + return
{value}
; + } + const Client = moduleReference(ClientImpl); + expect(() => { + const transport = ReactNoopFlightServer.render( + hi}} />, + ); + ReactNoopFlightClient.read(transport); + }).toErrorDev( + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' {hello: Math, title:

}\n' + + ' ^^^^', + {withoutStack: true}, + ); + }); + + it('should warn in DEV if a special object is passed to a nested array in Client Component', () => { + function ClientImpl({value}) { + return
{value}
; + } + const Client = moduleReference(ClientImpl); + expect(() => { + const transport = ReactNoopFlightServer.render( + hi

]} + />, + ); + ReactNoopFlightClient.read(transport); + }).toErrorDev( + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' [..., Math,

]\n' + + ' ^^^^', + {withoutStack: true}, + ); + }); + + it('should NOT warn in DEV for key getters', () => { + const transport = ReactNoopFlightServer.render(
); + ReactNoopFlightClient.read(transport); + }); + it('should warn in DEV if a class instance is passed to a host component', () => { class Foo { method() {} @@ -519,7 +694,7 @@ describe('ReactFlight', () => { ); ReactNoopFlightClient.read(transport); }).toErrorDev( - 'Only plain objects can be passed to client components from server components. ', + 'Only plain objects can be passed to Client Components from Server Components. ', {withoutStack: true}, ); }); @@ -577,9 +752,9 @@ describe('ReactFlight', () => { }); it('[TODO] it does not warn if you render a server element passed to a client module reference twice on the client when using useId', async () => { - // @TODO Today if you render a server component with useId and pass it to a client component and that client component renders the element in two or more + // @TODO Today if you render a Server Component with useId and pass it to a Client Component and that Client Component renders the element in two or more // places the id used on the server will be duplicated in the client. This is a deviation from the guarantees useId makes for Fizz/Client and is a consequence - // of the fact that the server component is actually rendered on the server and is reduced to a set of host elements before being passed to the Client component + // of the fact that the Server Component is actually rendered on the server and is reduced to a set of host elements before being passed to the Client component // so the output passed to the Client has no knowledge of the useId use. In the future we would like to add a DEV warning when this happens. For now // we just accept that it is a nuance of useId in Flight function App() { @@ -937,7 +1112,7 @@ describe('ReactFlight', () => { expect(ClientContext).toBe(undefined); - // Reset all modules, except flight-modules which keeps the registry of client components + // Reset all modules, except flight-modules which keeps the registry of Client Components const flightModules = require('react-noop-renderer/flight-modules'); jest.resetModules(); jest.mock('react-noop-renderer/flight-modules', () => flightModules); diff --git a/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js b/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js index 4f762230d9ee9..4270ad58617ce 100644 --- a/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js +++ b/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js @@ -50,7 +50,7 @@ describe('ReactFlightDOMRelay', () => { return model; } - it('can render a server component', () => { + it('can render a Server Component', () => { function Bar({text}) { return text.toUpperCase(); } @@ -85,7 +85,7 @@ describe('ReactFlightDOMRelay', () => { }); }); - it('can render a client component using a module reference and render there', () => { + it('can render a Client Component using a module reference and render there', () => { function UserClient(props) { return ( @@ -233,7 +233,7 @@ describe('ReactFlightDOMRelay', () => { ReactDOMFlightRelayServer.render(, transport); readThrough(transport); }).toErrorDev( - 'Only plain objects can be passed to client components from server components. ', + 'Only plain objects can be passed to Client Components from Server Components. ', {withoutStack: true}, ); }); diff --git a/packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js b/packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js index 35a2f6684c7cc..fee95c403db85 100644 --- a/packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js +++ b/packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js @@ -61,7 +61,7 @@ describe('ReactFlightNativeRelay', () => { return model; } - it('can render a server component', () => { + it('can render a Server Component', () => { function Bar({text}) { return {text.toUpperCase()}; } @@ -86,7 +86,7 @@ describe('ReactFlightNativeRelay', () => { expect(model).toMatchSnapshot(); }); - it('can render a client component using a module reference and render there', () => { + it('can render a Client Component using a module reference and render there', () => { function UserClient(props) { return ( @@ -132,7 +132,7 @@ describe('ReactFlightNativeRelay', () => { ); readThrough(transport); }).toErrorDev( - 'Only plain objects can be passed to client components from server components. ', + 'Only plain objects can be passed to Client Components from Server Components. ', {withoutStack: true}, ); }); diff --git a/packages/react-server-native-relay/src/__tests__/__snapshots__/ReactFlightNativeRelay-test.internal.js.snap b/packages/react-server-native-relay/src/__tests__/__snapshots__/ReactFlightNativeRelay-test.internal.js.snap index 4e7a196871b5f..cdeab0b8fb471 100644 --- a/packages/react-server-native-relay/src/__tests__/__snapshots__/ReactFlightNativeRelay-test.internal.js.snap +++ b/packages/react-server-native-relay/src/__tests__/__snapshots__/ReactFlightNativeRelay-test.internal.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ReactFlightNativeRelay can render a client component using a module reference and render there 1`] = ` +exports[`ReactFlightNativeRelay can render a Client Component using a module reference and render there 1`] = ` "1 RCTText null RCTRawText {\\"text\\":\\"Hello\\"} @@ -8,7 +8,7 @@ exports[`ReactFlightNativeRelay can render a client component using a module ref RCTRawText {\\"text\\":\\"Seb Smith\\"}" `; -exports[`ReactFlightNativeRelay can render a server component 1`] = ` +exports[`ReactFlightNativeRelay can render a Server Component 1`] = ` Object { "foo": Object { "bar": diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index b0d0875b41747..0fa01aac9b704 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -73,6 +73,8 @@ import { REACT_LAZY_TYPE, REACT_MEMO_TYPE, REACT_PROVIDER_TYPE, + REACT_SUSPENSE_TYPE, + REACT_SUSPENSE_LIST_TYPE, } from 'shared/ReactSymbols'; import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry'; @@ -195,6 +197,11 @@ function createRootContext( const POP = {}; +// Used for DEV messages to keep track of which parent rendered some props, +// in case they error. +const jsxPropsParents: WeakMap = new WeakMap(); +const jsxChildrenParents: WeakMap = new WeakMap(); + function readThenable(thenable: Thenable): T { if (thenable.status === 'fulfilled') { return thenable.value; @@ -226,12 +233,18 @@ function attemptResolveElement( // throw for functions. We could probably relax it to a DEV warning for other // cases. throw new Error( - 'Refs cannot be used in server components, nor passed to client components.', + 'Refs cannot be used in Server Components, nor passed to Client Components.', ); } + if (__DEV__) { + jsxPropsParents.set(props, type); + if (typeof props.children === 'object') { + jsxChildrenParents.set(props.children, type); + } + } if (typeof type === 'function') { if (isModuleReference(type)) { - // This is a reference to a client component. + // This is a reference to a Client Component. return [REACT_ELEMENT_TYPE, type, key, props]; } // This is a server-side component. @@ -253,7 +266,7 @@ function attemptResolveElement( // For key-less fragments, we add a small optimization to avoid serializing // it as a wrapper. // TODO: If a key is specified, we should propagate its key to any children. - // Same as if a server component has a key. + // Same as if a Server Component has a key. return props.children; } // This might be a built-in React component. We'll let the client decide. @@ -261,7 +274,7 @@ function attemptResolveElement( return [REACT_ELEMENT_TYPE, type, key, props]; } else if (type != null && typeof type === 'object') { if (isModuleReference(type)) { - // This is a reference to a client component. + // This is a reference to a Client Component. return [REACT_ELEMENT_TYPE, type, key, props]; } switch (type.$$typeof) { @@ -318,7 +331,7 @@ function attemptResolveElement( } } throw new Error( - `Unsupported server component type: ${describeValueForErrorMessage(type)}`, + `Unsupported Server Component type: ${describeValueForErrorMessage(type)}`, ); } @@ -505,66 +518,184 @@ function describeValueForErrorMessage(value: ReactModel): string { } } +function describeElementType(type: any): string { + if (typeof type === 'string') { + return type; + } + switch (type) { + case REACT_SUSPENSE_TYPE: + return 'Suspense'; + case REACT_SUSPENSE_LIST_TYPE: + return 'SuspenseList'; + } + if (typeof type === 'object') { + switch (type.$$typeof) { + case REACT_FORWARD_REF_TYPE: + return describeElementType(type.render); + case REACT_MEMO_TYPE: + return describeElementType(type.type); + case REACT_LAZY_TYPE: { + const lazyComponent: LazyComponent = (type: any); + const payload = lazyComponent._payload; + const init = lazyComponent._init; + try { + // Lazy may contain any component type so we recursively resolve it. + return describeElementType(init(payload)); + } catch (x) {} + } + } + } + return ''; +} + function describeObjectForErrorMessage( objectOrArray: | {+[key: string | number]: ReactModel, ...} | $ReadOnlyArray, expandedName?: string, ): string { + const objKind = objectName(objectOrArray); + if (objKind !== 'Object' && objKind !== 'Array') { + return objKind; + } + let str = ''; + let start = -1; + let length = 0; if (isArray(objectOrArray)) { - let str = '['; - const array: $ReadOnlyArray = objectOrArray; - for (let i = 0; i < array.length; i++) { - if (i > 0) { - str += ', '; - } - if (i > 6) { - str += '...'; - break; + if (__DEV__ && jsxChildrenParents.has(objectOrArray)) { + // Print JSX Children + const type = jsxChildrenParents.get(objectOrArray); + str = '<' + describeElementType(type) + '>'; + const array: $ReadOnlyArray = objectOrArray; + for (let i = 0; i < array.length; i++) { + const value = array[i]; + let substr; + if (typeof value === 'string') { + substr = value; + } else if (typeof value === 'object' && value !== null) { + // $FlowFixMe[incompatible-call] found when upgrading Flow + substr = '{' + describeObjectForErrorMessage(value) + '}'; + } else { + substr = '{' + describeValueForErrorMessage(value) + '}'; + } + if ('' + i === expandedName) { + start = str.length; + length = substr.length; + str += substr; + } else if (substr.length < 15 && str.length + substr.length < 40) { + str += substr; + } else { + str += '{...}'; + } } - const value = array[i]; - if ( - '' + i === expandedName && - typeof value === 'object' && - value !== null - ) { - // $FlowFixMe[incompatible-call] found when upgrading Flow - str += describeObjectForErrorMessage(value); - } else { - str += describeValueForErrorMessage(value); + str += ''; + } else { + // Print Array + str = '['; + const array: $ReadOnlyArray = objectOrArray; + for (let i = 0; i < array.length; i++) { + if (i > 0) { + str += ', '; + } + const value = array[i]; + let substr; + if (typeof value === 'object' && value !== null) { + // $FlowFixMe[incompatible-call] found when upgrading Flow + substr = describeObjectForErrorMessage(value); + } else { + substr = describeValueForErrorMessage(value); + } + if ('' + i === expandedName) { + start = str.length; + length = substr.length; + str += substr; + } else if (substr.length < 10 && str.length + substr.length < 40) { + str += substr; + } else { + str += '...'; + } } + str += ']'; } - str += ']'; - return str; } else { - let str = '{'; - const object: {+[key: string | number]: ReactModel, ...} = objectOrArray; - const names = Object.keys(object); - for (let i = 0; i < names.length; i++) { - if (i > 0) { - str += ', '; - } - if (i > 6) { - str += '...'; - break; + if (objectOrArray.$$typeof === REACT_ELEMENT_TYPE) { + str = '<' + describeElementType(objectOrArray.type) + '/>'; + } else if (__DEV__ && jsxPropsParents.has(objectOrArray)) { + // Print JSX + const type = jsxPropsParents.get(objectOrArray); + str = '<' + (describeElementType(type) || '...'); + const object: {+[key: string | number]: ReactModel, ...} = objectOrArray; + const names = Object.keys(object); + for (let i = 0; i < names.length; i++) { + str += ' '; + const name = names[i]; + str += describeKeyForErrorMessage(name) + '='; + const value = object[name]; + let substr; + if ( + name === expandedName && + typeof value === 'object' && + value !== null + ) { + // $FlowFixMe[incompatible-call] found when upgrading Flow + substr = describeObjectForErrorMessage(value); + } else { + substr = describeValueForErrorMessage(value); + } + if (typeof value !== 'string') { + substr = '{' + substr + '}'; + } + if (name === expandedName) { + start = str.length; + length = substr.length; + str += substr; + } else if (substr.length < 10 && str.length + substr.length < 40) { + str += substr; + } else { + str += '...'; + } } - const name = names[i]; - str += describeKeyForErrorMessage(name) + ': '; - const value = object[name]; - if ( - name === expandedName && - typeof value === 'object' && - value !== null - ) { - // $FlowFixMe[incompatible-call] found when upgrading Flow - str += describeObjectForErrorMessage(value); - } else { - str += describeValueForErrorMessage(value); + str += '>'; + } else { + // Print Object + str = '{'; + const object: {+[key: string | number]: ReactModel, ...} = objectOrArray; + const names = Object.keys(object); + for (let i = 0; i < names.length; i++) { + if (i > 0) { + str += ', '; + } + const name = names[i]; + str += describeKeyForErrorMessage(name) + ': '; + const value = object[name]; + let substr; + if (typeof value === 'object' && value !== null) { + // $FlowFixMe[incompatible-call] found when upgrading Flow + substr = describeObjectForErrorMessage(value); + } else { + substr = describeValueForErrorMessage(value); + } + if (name === expandedName) { + start = str.length; + length = substr.length; + str += substr; + } else if (substr.length < 10 && str.length + substr.length < 40) { + str += substr; + } else { + str += '...'; + } } + str += '}'; } - str += '}'; + } + if (expandedName === undefined) { return str; } + if (start > -1 && length > 0) { + const highlight = ' '.repeat(start) + '^'.repeat(length); + return '\n ' + str + '\n ' + highlight; + } + return '\n ' + str; } let insideContextProps = null; @@ -580,14 +711,30 @@ export function resolveModelToJSON( // $FlowFixMe const originalValue = parent[key]; if (typeof originalValue === 'object' && 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), - ); + if (objectName(originalValue) !== 'Object') { + const jsxParentType = jsxChildrenParents.get(parent); + if (typeof jsxParentType === 'string') { + console.error( + '%s objects cannot be rendered as text children. Try formatting it using toString().%s', + objectName(originalValue), + describeObjectForErrorMessage(parent, key), + ); + } else { + console.error( + 'Only plain objects can be passed to Client Components from Server Components. ' + + '%s objects are not supported.%s', + objectName(originalValue), + describeObjectForErrorMessage(parent, key), + ); + } + } else { + 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.%s', + describeObjectForErrorMessage(parent, key), + ); + } } } @@ -612,7 +759,7 @@ export function resolveModelToJSON( } } - // Resolve server components. + // Resolve Server Components. while ( typeof value === 'object' && value !== null && @@ -630,7 +777,7 @@ export function resolveModelToJSON( case REACT_ELEMENT_TYPE: { // TODO: Concatenate keys of parents onto children. const element: React$Element = (value: any); - // Attempt to render the server component. + // Attempt to render the Server Component. value = attemptResolveElement( element.type, element.key, @@ -716,30 +863,24 @@ export function resolveModelToJSON( // 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', + 'Only plain objects can be passed to Client Components from Server Components. ' + + '%s objects are not supported.%s', objectName(value), - describeKeyForErrorMessage(key), - describeObjectForErrorMessage(parent), + describeObjectForErrorMessage(parent, key), ); } 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), + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Classes or other objects with methods are not supported.%s', describeObjectForErrorMessage(parent, key), ); } 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', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like %s are not supported.%s', symbols[0].description, - describeKeyForErrorMessage(key), describeObjectForErrorMessage(parent, key), ); } @@ -769,24 +910,15 @@ export function resolveModelToJSON( } if (/^on[A-Z]/.test(key)) { throw new Error( - 'Event handlers cannot be passed to client component props. ' + - `Remove ${describeKeyForErrorMessage( - key, - )} from these props if possible: ${describeObjectForErrorMessage( - parent, - )} -` + - 'If you need interactivity, consider converting part of this to a client component.', + 'Event handlers cannot be passed to Client Component props.' + + describeObjectForErrorMessage(parent, key) + + '\nIf you need interactivity, consider converting part of this to a Client Component.', ); } else { throw new Error( - 'Functions cannot be passed directly to client components ' + - "because they're not serializable. " + - `Remove ${describeKeyForErrorMessage(key)} (${value.displayName || - value.name || - 'function'}) from this object, or avoid the entire object: ${describeObjectForErrorMessage( - parent, - )}`, + 'Functions cannot be passed directly to Client Components ' + + "because they're not serializable." + + describeObjectForErrorMessage(parent, key), ); } } @@ -802,16 +934,12 @@ export function resolveModelToJSON( if (Symbol.for(name) !== value) { throw new Error( - 'Only global symbols received from Symbol.for(...) can be passed to client components. ' + + 'Only global symbols received from Symbol.for(...) can be passed to Client Components. ' + `The symbol Symbol.for(${ // $FlowFixMe `description` might be undefined value.description - }) cannot be found among global symbols. ` + - `Remove ${describeKeyForErrorMessage( - key, - )} from this object, or avoid the entire object: ${describeObjectForErrorMessage( - parent, - )}`, + }) cannot be found among global symbols.` + + describeObjectForErrorMessage(parent, key), ); } @@ -825,22 +953,14 @@ export function resolveModelToJSON( // $FlowFixMe: bigint isn't added to Flow yet. if (typeof value === 'bigint') { throw new Error( - `BigInt (${value}) is not yet supported in client component props. ` + - `Remove ${describeKeyForErrorMessage( - key, - )} from this object or use a plain number instead: ${describeObjectForErrorMessage( - parent, - )}`, + `BigInt (${value}) is not yet supported in Client Component props.` + + describeObjectForErrorMessage(parent, key), ); } throw new Error( - `Type ${typeof value} is not supported in client component props. ` + - `Remove ${describeKeyForErrorMessage( - key, - )} from this object, or avoid the entire object: ${describeObjectForErrorMessage( - parent, - )}`, + `Type ${typeof value} is not supported in Client Component props.` + + describeObjectForErrorMessage(parent, key), ); } @@ -968,7 +1088,7 @@ function retryTask(request: Request, task: Task): void { // previous attempt. const prevThenableState = task.thenableState; - // Attempt to render the server component. + // Attempt to render the Server Component. // Doing this here lets us reuse this same task if the next component // also suspends. task.model = value; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 278c8d464089a..23afdb85c34cd 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -338,7 +338,7 @@ "348": "ensureListeningTo(): received a container that was not an element node. This is likely a bug in React.", "349": "Expected a work-in-progress root. This is a bug in React. Please file an issue.", "350": "Cannot read from mutable source during the current render without tearing. This may be a bug in React. Please file an issue.", - "351": "Unsupported server component type: %s", + "351": "Unsupported Server Component type: %s", "352": "React Lazy Components are not yet supported on the server.", "353": "A server block should never encode any other slots. This is a bug in React.", "354": "getInspectorDataForViewAtPoint() is not available in production.", @@ -360,12 +360,12 @@ "371": "Text string must be rendered within a component.\n\nText: %s", "372": "Cannot call unstable_createEventHandle with \"%s\", as it is not an event known to React.", "373": "This Hook is not supported in Server Components.", - "374": "Event handlers cannot be passed to client component props. Remove %s from these props if possible: %s\nIf you need interactivity, consider converting part of this to a client component.", - "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", - "376": "Only global symbols received from Symbol.for(...) can be passed to client components. The symbol Symbol.for(%s) cannot be found among global symbols. Remove %s from this object, or avoid the entire object: %s", - "377": "BigInt (%s) is not yet supported in client component props. Remove %s from this object or use a plain number instead: %s", - "378": "Type %s is not supported in client component props. Remove %s from this object, or avoid the entire object: %s", - "379": "Refs cannot be used in server components, nor passed to client components.", + "374": "Event handlers cannot be passed to Client Component props.%s\nIf you need interactivity, consider converting part of this to a Client Component.", + "375": "Functions cannot be passed directly to Client Components because they're not serializable.%s", + "376": "Only global symbols received from Symbol.for(...) can be passed to Client Components. The symbol Symbol.for(%s) cannot be found among global symbols.%s", + "377": "BigInt (%s) is not yet supported in Client Component props.%s", + "378": "Type %s is not supported in Client Component props.%s", + "379": "Refs cannot be used in Server Components, nor passed to Client Components.", "380": "Reading the cache is only supported while rendering.", "381": "This feature is not supported by ReactSuspenseTestUtils.", "382": "This query has received more parameters than the last time the same query was used. Always pass the exact number of parameters that the query needs.",