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.",