Repository: facebook/react
Pull request: toString children of title

children of title can either behave like children or like an attribute. We're kind of treating it more like an attribute now so we should support toString/valueOf like we do on attributes.

Changed files:
filename: packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
patch: @@ -587,17 +587,27 @@ export function getResource(
       return null;
     }
     case 'title': {
-      let child = pendingProps.children;
-      if (Array.isArray(child) && child.length === 1) {
-        child = child[0];
+      const children = pendingProps.children;
+      let child;
+      if (Array.isArray(children)) {
+        child = children.length === 1 ? children[0] : null;
+      } else {
+        child = children;
       }
-      if (typeof child === 'string' || typeof child === 'number') {
+      if (
+        typeof child !== 'function' &&
+        typeof child !== 'symbol' &&
+        child !== null &&
+        child !== undefined
+      ) {
+        // eslint-disable-next-line react-internal/safe-string-coercion
+        const childString = '' + (child: any);
         const headRoot: Document = getDocumentFromRoot(resourceRoot);
         const headResources = getResourcesFromRoot(headRoot).head;
-        const key = getTitleKey(child);
+        const key = getTitleKey(childString);
         let resource = headResources.get(key);
         if (!resource) {
-          const titleProps = titlePropsFromRawProps(child, pendingProps);
+          const titleProps = titlePropsFromRawProps(childString, pendingProps);
           resource = {
             type: 'title',
             props: titleProps,

filename: packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
patch: @@ -652,17 +652,27 @@ export function resourcesFromElement(type: string, props: Props): boolean {
   const resources = currentResources;
   switch (type) {
     case 'title': {
-      let child = props.children;
-      if (Array.isArray(child) && child.length === 1) {
-        child = child[0];
+      const children = props.children;
+      let child;
+      if (Array.isArray(children)) {
+        child = children.length === 1 ? children[0] : null;
+      } else {
+        child = children;
       }
-      if (typeof child === 'string' || typeof child === 'number') {
-        const key = 'title::' + child;
+      if (
+        typeof child !== 'function' &&
+        typeof child !== 'symbol' &&
+        child !== null &&
+        child !== undefined
+      ) {
+        // eslint-disable-next-line react-internal/safe-string-coercion
+        const childString = '' + (child: any);
+        const key = 'title::' + childString;
         let resource = resources.headsMap.get(key);
         if (!resource) {
           resource = {
             type: 'title',
-            props: titlePropsFromRawProps(child, props),
+            props: titlePropsFromRawProps(childString, props),
             flushed: false,
           };
           resources.headsMap.set(key, resource);

filename: packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
patch: @@ -1473,12 +1473,19 @@ function pushTitleImpl(
   }
   target.push(endOfStartTag);
 
-  const child =
-    Array.isArray(children) && children.length < 2
-      ? children[0] || null
-      : children;
-  if (typeof child === 'string' || typeof child === 'number') {
-    target.push(stringToChunk(escapeTextForBrowser(child)));
+  const child = Array.isArray(children)
+    ? children.length < 2
+      ? children[0]
+      : null
+    : children;
+  if (
+    typeof child !== 'function' &&
+    typeof child !== 'symbol' &&
+    child !== null &&
+    child !== undefined
+  ) {
+    // eslint-disable-next-line react-internal/safe-string-coercion
+    target.push(stringToChunk(escapeTextForBrowser('' + child)));
   }
   target.push(endTag1, stringToChunk('title'), endTag2);
   return null;

filename: packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
patch: @@ -5144,8 +5144,10 @@ describe('ReactDOMFizzServer', () => {
         }
 
         if (gate(flags => flags.enableFloat)) {
-          // invalid titles are not emitted on the server when float is on
-          expect(getVisibleChildren(container)).toEqual(undefined);
+          // object titles are toStringed when float is on
+          expect(getVisibleChildren(container)).toEqual(
+            <title>{'[object Object]'}</title>,
+          );
         } else {
           expect(getVisibleChildren(container)).toEqual(<title>hello</title>);
         }
@@ -5159,8 +5161,10 @@ describe('ReactDOMFizzServer', () => {
         expect(Scheduler).toFlushAndYield([]);
         expect(errors).toEqual([]);
         if (gate(flags => flags.enableFloat)) {
-          // invalid titles are not emitted on the server when float is on
-          expect(getVisibleChildren(container)).toEqual(undefined);
+          // object titles are toStringed when float is on
+          expect(getVisibleChildren(container)).toEqual(
+            <title>{'[object Object]'}</title>,
+          );
         } else {
           expect(getVisibleChildren(container)).toEqual(<title>hello</title>);
         }