Repository: facebook/react
Pull request: [Flight] Better error message if you pass a function as a child to a client component

Similar to #28362 but if you pass it to a client component.

Changed files:
filename: packages/react-client/src/__tests__/ReactFlight-test.js
patch: @@ -689,7 +689,7 @@ describe('ReactFlight', () => {
       );
     }
     function FunctionProp() {
-      return <div>{() => {}}</div>;
+      return <div>{function fn() {}}</div>;
     }
     function SymbolProp() {
       return <div foo={Symbol('foo')} />;
@@ -707,8 +707,11 @@ describe('ReactFlight', () => {
         </Client>
       );
     }
+    function FunctionChildrenClient() {
+      return <Client>{function Component() {}}</Client>;
+    }
     function FunctionPropClient() {
-      return <Client>{() => {}}</Client>;
+      return <Client foo={() => {}} />;
     }
     function SymbolPropClient() {
       return <Client foo={Symbol('foo')} />;
@@ -731,6 +734,10 @@ describe('ReactFlight', () => {
       <EventHandlerPropClient />,
       options,
     );
+    const fnChildrenClient = ReactNoopFlightServer.render(
+      <FunctionChildrenClient />,
+      options,
+    );
     const fnClient = ReactNoopFlightServer.render(
       <FunctionPropClient />,
       options,
@@ -754,7 +761,9 @@ describe('ReactFlight', () => {
             </ErrorBoundary>
             <ErrorBoundary
               expectedMessage={
-                'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
+                __DEV__
+                  ? 'Functions are not valid as a child of Client Components. This may happen if you return fn instead of <fn /> 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".'
               }>
               <Render promise={ReactNoopFlightClient.read(fn)} />
             </ErrorBoundary>
@@ -767,6 +776,14 @@ describe('ReactFlight', () => {
             <ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
               <Render promise={ReactNoopFlightClient.read(eventClient)} />
             </ErrorBoundary>
+            <ErrorBoundary
+              expectedMessage={
+                __DEV__
+                  ? 'Functions are not valid as a child of Client Components. This may happen if you return Component instead of <Component /> 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".'
+              }>
+              <Render promise={ReactNoopFlightClient.read(fnChildrenClient)} />
+            </ErrorBoundary>
             <ErrorBoundary
               expectedMessage={
                 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
@@ -945,8 +962,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' +
-        '  <input value={{toJSON: function}}>\n' +
-        '               ^^^^^^^^^^^^^^^^^^^^',
+        '  <input value={{toJSON: ...}}>\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},
     );
   });

filename: packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
patch: @@ -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' +
+          '                          ^^^^^^^^^^^^^^^^^^^^^',
       );
     }
   });

filename: packages/react-server/src/ReactFlightServer.js
patch: @@ -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),
       );
     }

filename: packages/shared/ReactSerializationErrors.js
patch: @@ -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);

filename: scripts/error-codes/codes.json
patch: @@ -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"
 }