react-helmet icon indicating copy to clipboard operation
react-helmet copied to clipboard

JSX Fragments

Open alexlrobertson opened this issue 7 years ago • 20 comments

React introduced JSX fragments in version 16.2.0.

When attempting to use a fragment inside a <Helmet> component I get this error: You may be attempting to nest <Helmet> components within each other, which is not allowed. Refer to our API for more information.

alexlrobertson avatar Dec 20 '17 15:12 alexlrobertson

Seems like there is a check for valid tags and fragments are not supported yet: https://github.com/nfl/react-helmet/blob/master/src/HelmetConstants.js#L7 https://github.com/nfl/react-helmet/blob/master/src/Helmet.js#L185

xRahul avatar Dec 22 '17 17:12 xRahul

Is JSX Fragment support a feature already being considered for development on React Helmet, or would outside contributions be welcomed to add this feature (as per the contribution guidelines)?

im-grahammartin avatar May 29 '18 14:05 im-grahammartin

+1 here just ran into this when doing something like

export default function PageMeta({
  $state: {
    selected: { meta },
  },
  ...props
}: PageMetaProps) {
  return (
    <Helmet>
      <meta charSet="utf-8" />
      <title>
        {meta.title}
      </title>
      <OpenGraph {...props} />
    </Helmet>
  );
}

type OpenGraphProps = {
  title?: string,
  description?: string,
  image?: string,
  url?: string,
  type?: string,
  cardType?: 'summary' | 'summary_large_image' | 'app' | 'player',
  creator: string,
};
function OpenGraph({
  title, description, image, url, type, cardType, creator,
}: OpenGraphProps) {
  return (
    <React.Fragment>
      {title && <meta property="og:title" content={title} />}
      {description && <meta property="og:description" content={description} />}
      {image && <meta property="og:image" content={image} />}
      {url && <meta property="og:url" content={url} />}
      {type && <meta property="og:type" content={type} />}
      {cardType && <meta name="twitter:card" content={cardType || 'summary_large_image'} />}
      <meta name="twitter:creator" content={creator} />
    </React.Fragment>
  );
}

bradennapier avatar Jun 22 '18 18:06 bradennapier

I've investigated this issue a bit with the intent of putting in a PR, and it's not as simple as just editing HelmetConstants.js:

  • React version needs to be upgraded to 16
  • PhantomJS needs to be polyfilled for ES6 Set as a result
  • Karma throws because it's trying to stringify a symbol
  • Fragment children can be either an object or an array, depending on whether n > 1, and then the children themselves need to be flattened into the broader array of children (i.e., Fragment elements need to be effectively handled like arrays).

I'd say I'm about 50% of the way to a pull request but want some indication from the maintainers that it'll be accepted/I'm on the right track before I invest the effort. I think it's a worthwhile addition to the library, both because Fragments are part of standard React and also because it enables conditional patterns like the one @bradennapier discusses above.

aendra-rininsland avatar Jul 30 '18 13:07 aendra-rininsland

Currently you can use a function to convert from Fragments to array of keyed elements. Something like this:

renderFragments() {
  const fragments = [this.renderVerificationMeta(), this.renderIcons()];

  return fragments.reduce(
    (acc, { props: { children } }, index) =>
      acc.concat(
        React.Children.map(children, (child, childIndex) =>
          React.cloneElement(child, {
            // eslint-disable-next-line react/no-array-index-key
            key: `${index}-${childIndex}`,
          }),
        ),
      ),
    [],
  );
}

renderVerificationMeta() {
  return (
    <>
      <meta
        name="google-site-verification"
        content="..."
      />
    </>
  );
}

This will at least free you from typing key manually

SleepWalker avatar Oct 24 '18 07:10 SleepWalker

Is there any update on this? React 16.2.0 came out just about a year ago, and Helmet is still on 15.x.

I just ran into this issue myself, and it took me several hours to determine the issue, thanks to cryptic error messages. The following are the errors thrown (in the listed numbers) for a single instance of Fragment. None of these stack traces goes back to my own code, and none of them suggests what the underlying issue is.

Error 1 (x8)

Helmet.js:133 Uncaught TypeError: Cannot convert a Symbol value to a string
    at ProxyComponent.warnOnInvalidChildren (Helmet.js:133)
    at ProxyComponent.warnOnInvalidChildren (react-hot-loader.development.js:648)
    at Helmet.js:162
    at forEachSingleChild (react.development.js:1139)
    at traverseAllChildrenImpl (react.development.js:1043)
    at traverseAllChildrenImpl (react.development.js:1059)
    at traverseAllChildren (react.development.js:1114)
    at Object.forEachChildren [as forEach] (react.development.js:1159)
    at ProxyComponent.mapChildrenToProps (Helmet.js:151)
    at ProxyComponent.mapChildrenToProps (react-hot-loader.development.js:648)
    at ProxyComponent.render (Helmet.js:201)
    at finishClassComponent (react-dom.development.js:14301)
    at updateClassComponent (react-dom.development.js:14264)
    at beginWork (react-dom.development.js:15082)
    at performUnitOfWork (react-dom.development.js:17820)
    at workLoop (react-dom.development.js:17860)
    at HTMLUnknownElement.callCallback (react-dom.development.js:149)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:199)
    at invokeGuardedCallback (react-dom.development.js:256)
    at replayUnitOfWork (react-dom.development.js:17107)
    at renderRoot (react-dom.development.js:17979)
    at performWorkOnRoot (react-dom.development.js:18837)
    at performWork (react-dom.development.js:18749)
    at performSyncWork (react-dom.development.js:18723)
    at requestWork (react-dom.development.js:18592)
    at scheduleWork (react-dom.development.js:18401)
    at scheduleRootUpdate (react-dom.development.js:19069)
    at updateContainerAtExpirationTime (react-dom.development.js:19097)
    at updateContainer (react-dom.development.js:19154)
    at ReactRoot../node_modules/react-dom/cjs/react-dom.development.js.ReactRoot.render (react-dom.development.js:19416)
    at react-dom.development.js:19556
    at unbatchedUpdates (react-dom.development.js:18952)
    at legacyRenderSubtreeIntoContainer (react-dom.development.js:19552)
    at render (react-dom.development.js:19613)
    at app.js:62
Error 2 (x3)

index.js:2177 The above error occurred in the <HelmetWrapper> component:
    in HelmetWrapper (at post.tsx:219)
    in div (created by IndexLayout)
    in IndexLayout (created by PageTemplate)
    in PageTemplate (created by PageRenderer)
    in PageRenderer (created by JSONStore)
    in JSONStore (created by EnsureResources)
    in ScrollContext (created by EnsureResources)
    in RouteUpdates (created by EnsureResources)
    in EnsureResources (created by RouteHandler)
    in RouteHandler (created by Root)
    in div (created by FocusHandlerImpl)
    in FocusHandlerImpl (created by Context.Consumer)
    in FocusHandler (created by RouterImpl)
    in RouterImpl (created by LocationProvider)
    in LocationProvider (created by Context.Consumer)
    in Location (created by Context.Consumer)
    in Router (created by Root)
    in Root
    in _default (created by HotExported_default)
    in AppContainer (created by HotExported_default)
    in HotExported_default

React will try to recreate this component tree from scratch using the error boundary you provided, LocationProvider.
Error 3 (x2)

index.js:2177 The above error occurred in the <LocationProvider> component:
    in LocationProvider (created by Context.Consumer)
    in Location (created by Context.Consumer)
    in Router (created by Root)
    in Root
    in _default (created by HotExported_default)
    in AppContainer (created by HotExported_default)
    in HotExported_default

React will try to recreate this component tree from scratch using the error boundary you provided, AppContainer.

MartinRosenberg avatar Dec 11 '18 09:12 MartinRosenberg

I also stumbled into this problem. Are there any plans of allowing fragments as children?

proProbe avatar Dec 11 '18 14:12 proProbe

I've got the same problem. It would really be great to get this working.

mikestopcontinues avatar Jan 21 '19 17:01 mikestopcontinues

Any updates on this?

aksel avatar May 08 '19 21:05 aksel

Hitting the same issue here.

andrewplummer avatar Aug 09 '19 15:08 andrewplummer

A workaround is to have different <Helmet>'s:

{title && (
  <Helmet>
    <title>{title}</title>
  </Helmet>
)}
{description && (
  <Helmet>
    <meta name="description" content={description} />
    <meta itemProp="description" content={description} />
    <meta name="twitter:description" content={description} />
    <meta property="og:description" content={description} />
  </Helmet>
)}

gpbl avatar Aug 12 '19 17:08 gpbl

👍 ok that works... not as clean but cleaner than having arrays + keys

andrewplummer avatar Aug 14 '19 02:08 andrewplummer

:eyes:

giancarlosisasi avatar Nov 06 '19 14:11 giancarlosisasi

At this point, it looks like this library is abandoned. React 16 came out over 2 years ago, and the last commit here of any kind was over half a year ago.

If you want to use a current version of React with Helmet, your best bet is to either fork it, or use https://github.com/staylor/react-helmet-async which also sheds the problematic dependency on react-side-effect.

MartinRosenberg avatar Nov 06 '19 22:11 MartinRosenberg

Or, if you have an array of meta data:

{metaData &&  metaData.map(meta => (
  <Helmet>
    <meta name={meta.name} content={meta.description} />
  </Helmet>
))}

assainov avatar Dec 17 '19 13:12 assainov

Being able to use fragments would allow me to rewrite this messy piece of code using a map:

<link rel="preconnect" href="https://connect.facebook.net/" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://connect.facebook.net/" />
<link rel="preconnect" href="https://static.hotjar.com/" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://static.hotjar.com/" />
<link rel="preconnect" href="https://www.facebook.com/" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://www.facebook.com/" />
<link rel="preconnect" href="https://www.google-analytics.com/" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://www.google-analytics.com/" />
<link rel="preconnect" href="https://www.google.de/" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://www.google.de/" />
<link rel="preconnect" href="https://www.googletagmanager.com/" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://www.googletagmanager.com/" />

adonig avatar Mar 23 '20 00:03 adonig

fragments have been around for almost 4 years now, they ever gonna add support?

GeorgeWL avatar Nov 09 '20 11:11 GeorgeWL

I'm not sure how others are getting around this, but I have a way I don't hate. Create a function that populates the array of relevant data:

const getFacebookProperties = () => {
  let data = [
    { property: "test-1", content: "value-a" },
    { property: "test-2", content: "value-b" },
    { property: "test-3", content: "value-c" },
  ];

  // add additional properties as needed 

  return data;
}

Then map it in the Helmet:

    <Helmet>
      {getFacebookProperties().map(c => <meta {...c} />)}
    </Helmet>

Oyyou avatar Dec 24 '20 14:12 Oyyou

I'm using it like that:

<BasicSEOHeaders
  title="Page Title | Website name"
  description="Page description"
/>
import React from "react";
import { Helmet } from "react-helmet";

const BasicSEOHeaders = (props) => {
  return (
    <Helmet>
      <meta property="og:type" content="website" />
      <meta property="og:url" content={`${window.location.href}`} />

      {props.title && <title>{props.title}</title>}
      {props.title && <meta property="og:title" content={props.title} />}

      {props.description && <meta name="description" content={props.description} /> }
      {props.description && <meta property="og:description" content={props.description} /> }

      {(props.title && props.description) && (
        <script type="application/ld+json">
          {JSON.stringify({
            "@context": "https://schema.org",
            "@type": "WebPage",
            url: window.location.href,
            name: props.title,
            description: props.description
          })}
        </script>
      )}
    </Helmet>
  );
};

export default BasicSEOHeaders;

LosTigeros avatar Jan 21 '21 11:01 LosTigeros

Being able to use fragments would allow me to rewrite this messy piece of code using a map:

<link rel="preconnect" href="https://connect.facebook.net/" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://connect.facebook.net/" />
<link rel="preconnect" href="https://static.hotjar.com/" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://static.hotjar.com/" />

One solution is to to return an array of JSX elements, instead of wrapping it in fragments:

{map(prefetch, (link, vendor) => (
  [
    <link key={`preconnect-${vendor}`} rel="preconnect" href={link} />,
    <link key={`dns-prefetch-${vendor}`}  rel="dns-prefetch" href={link} />
  ]
))}

mkhamash avatar Jun 30 '21 10:06 mkhamash