From e97e030a8307546ae3f5e97c8b85e443eb5df175 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 17 Feb 2024 22:48:46 -0500 Subject: [PATCH] Better error message if you pass a function as a child to a client component --- .../src/__tests__/ReactFlight-test.js | 31 ++++++++++++++----- .../src/__tests__/ReactFlightDOM-test.js | 14 +++++---- .../react-server/src/ReactFlightServer.js | 19 +++++++++++- packages/shared/ReactSerializationErrors.js | 6 ++-- scripts/error-codes/codes.json | 5 +-- 5 files changed, 57 insertions(+), 18 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 06dee1d54ed22..5508a52c920d9 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -689,7 +689,7 @@ describe('ReactFlight', () => { ); } function FunctionProp() { - return
{() => {}}
; + return
{function fn() {}}
; } function SymbolProp() { return
; @@ -707,8 +707,11 @@ describe('ReactFlight', () => { ); } + function FunctionChildrenClient() { + return {function Component() {}}; + } function FunctionPropClient() { - return {() => {}}; + return {}} />; } function SymbolPropClient() { return ; @@ -731,6 +734,10 @@ describe('ReactFlight', () => { , options, ); + const fnChildrenClient = ReactNoopFlightServer.render( + , + options, + ); const fnClient = ReactNoopFlightServer.render( , options, @@ -754,7 +761,9 @@ describe('ReactFlight', () => { from render. Or maybe you meant to call this function rather than return it.' + : 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".' }> @@ -767,6 +776,14 @@ describe('ReactFlight', () => { + from render. Or maybe you meant to call this function rather than return it.' + : 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".' + }> + + { '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.\n' + - ' \n' + - ' ^^^^^^^^^^^^^^^^^^^^', + ' \n' + + ' ^^^^^^^^^^^^^^^', {withoutStack: true}, ); }); @@ -1035,8 +1052,8 @@ describe('ReactFlight', () => { '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.\n' + - ' <>Current date: {{toJSON: function}}\n' + - ' ^^^^^^^^^^^^^^^^^^^^', + ' <>Current date: {{toJSON: ...}}\n' + + ' ^^^^^^^^^^^^^^^', {withoutStack: true}, ); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index e336e2ed8cf90..99a8c79a62a85 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -1710,15 +1710,17 @@ describe('ReactFlightDOM', () => { expect(reportedErrors.length).toBe(1); if (__DEV__) { expect(reportedErrors[0].message).toEqual( - 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".\n' + - ' <... prop={client} invalid={function}>\n' + - ' ^^^^^^^^^^', + 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". ' + + 'Or maybe you meant to call this function rather than return it.\n' + + ' <... prop={client} invalid={function InvalidValue}>\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^', ); } else { expect(reportedErrors[0].message).toEqual( - 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".\n' + - ' {prop: client, invalid: function}\n' + - ' ^^^^^^^^', + 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". ' + + 'Or maybe you meant to call this function rather than return it.\n' + + ' {prop: client, invalid: function InvalidValue}\n' + + ' ^^^^^^^^^^^^^^^^^^^^^', ); } }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 072a71b03dcc5..e5a3dda775959 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1554,10 +1554,27 @@ function renderModelDestructive( describeObjectForErrorMessage(parent, parentPropertyName) + '\nIf you need interactivity, consider converting part of this to a Client Component.', ); + } else if ( + __DEV__ && + (jsxChildrenParents.has(parent) || + (jsxPropsParents.has(parent) && parentPropertyName === 'children')) + ) { + const componentName = value.displayName || value.name || 'Component'; + throw new Error( + 'Functions are not valid as a child of Client Components. This may happen if ' + + 'you return ' + + componentName + + ' instead of <' + + componentName + + ' /> from render. ' + + 'Or maybe you meant to call this function rather than return it.' + + describeObjectForErrorMessage(parent, parentPropertyName), + ); } else { throw new Error( 'Functions cannot be passed directly to Client Components ' + - 'unless you explicitly expose it by marking it with "use server".' + + 'unless you explicitly expose it by marking it with "use server". ' + + 'Or maybe you meant to call this function rather than return it.' + describeObjectForErrorMessage(parent, parentPropertyName), ); } diff --git a/packages/shared/ReactSerializationErrors.js b/packages/shared/ReactSerializationErrors.js index 5dbb79a704fc6..fb5181a5a9151 100644 --- a/packages/shared/ReactSerializationErrors.js +++ b/packages/shared/ReactSerializationErrors.js @@ -107,11 +107,13 @@ export function describeValueForErrorMessage(value: mixed): string { } return name; } - case 'function': + case 'function': { if ((value: any).$$typeof === CLIENT_REFERENCE_TAG) { return describeClientReference(value); } - return 'function'; + const name = (value: any).displayName || value.name; + return name ? 'function ' + name : 'function'; + } default: // eslint-disable-next-line react-internal/safe-string-coercion return String(value); diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 6153624f690cf..8f6a681c2392d 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -361,7 +361,7 @@ "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.%s\nIf you need interactivity, consider converting part of this to a Client Component.", - "375": "Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with \"use server\".%s", + "375": "Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with \"use server\". Or maybe you meant to call this function rather than return it.%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", @@ -490,5 +490,6 @@ "502": "Cannot read a Client Context from a Server Component.", "503": "Cannot use() an already resolved Client Reference.", "504": "Failed to read a RSC payload created by a development version of React on the server while using a production version on the client. Always use matching versions on the server and the client.", - "505": "Cannot render an Async Component, Promise or React.Lazy inside React.Children. We recommend not iterating over children and just rendering them plain." + "505": "Cannot render an Async Component, Promise or React.Lazy inside React.Children. We recommend not iterating over children and just rendering them plain.", + "506": "Functions are not valid as a child of Client Components. This may happen if you return %s instead of <%s /> from render. Or maybe you meant to call this function rather than return it.%s" }