react-helmet-async: Helmet tag not being updated in production with server rendering

HI @staylor! While implementing this library with server rendering, I am noticing that while the updated tags show up in the server-rendered code in development, they do not do show up in production.

Here is my code on the server side. Specifically helmetContext.helmet.meta.toString shows up as empty in production while it works perfectly fine in development. I’m on React 16.5.2, Webpack 4.16 and Node 10.

// Holds Helmet state specific to each request
const helmetContext = {}

// Declare our React application.
const app = (
  <AsyncComponentProvider asyncContext = { asyncComponentsContext }>
    <JobProvider jobContext = { jobContext }>
      <StaticRouter location = { request.url } context = { reactRouterContext }>
        <HelmetProvider context = { helmetContext } >
          <Provider store = { store }>
            <CookiesProvider cookies = { request.universalCookies }>
              <UserAgentProvider ua = {request.headers["user-agent"]} >
                  <IntlProvider
                    locale		= { locale }
                    messages	= { messages }
                    initialNow	= { Date.now() }
                    textComponent = { Fragment }
                  >
                    <Route component = { App } />
                  </IntlProvider>
              </UserAgentProvider>
            </CookiesProvider>
          </Provider>
        </HelmetProvider>
      </StaticRouter>
    </JobProvider>
  </AsyncComponentProvider>
)

// ℹ️ First we bootstrap our app to ensure the async components/data are resolved
await asyncBootstrapper( app )

const jsx = sheet.collectStyles( app )
const stream = sheet.interleaveWithNodeStream(
  renderToNodeStream( jsx ),
)

// Resolve the assets (js/css) for the client bundle's entry chunk.
const clientEntryAssets = getClientBundleEntryAssets()

const { helmet } = helmetContext

/*
* all of the things in the params below are included in the header
* can add htmlAttributes & bodyAttributes from Helmet if needed
*/
const paramsForHeader = {
  titleTag:	helmet.title.toString(),
  metaTags:	helmet.meta.toString(),
  linkTags:	helmet.link.toString(),
  cssBundle:	clientEntryAssets && clientEntryAssets.css,
  nonce,
}

console.log( "metaTags toString:", helmet.meta.toString())  // nothing
console.log( "metaTags toComponent", helmet.meta.toComponent()) // nothing either

/*
* all of the things in the params below are included in the footer
*/
const paramsForFooter = {
  storeState:	store.getState(),
  routerState: reactRouterContext,
  jobsState:	jobContext.getState(),
  asyncComponentsState:	asyncComponentsContext.getState(),
  jsBundle:		clientEntryAssets.js,
  clientConfig,
  polyfillPath,
  nonce,
}

switch ( reactRouterContext.status ) {
  case 301:
  case 302:
  // ...
      break

  case 404:
  // ...
      break

  default:
      // Otherwise everything is all good and we send a 200 OK status.
      response.status( 200 )
      response.type( "html" )
      response.setHeader( "Cache-Control", "no-cache" )
      response.write( Head( paramsForHeader ))
      stream.pipe( response, { end: false })
      stream.on( "end", () => response.end( Footer( paramsForFooter )))
}

Any ideas what might be going on?

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 1
  • Comments: 15 (2 by maintainers)

Most upvoted comments

The problem is it is a stream, and it is async process. the moment your read the variable the stream is not done for read yet. Therefore you need to attach event on('end', cb) on this stream, so you could see the value. See my sample implementation here.

      const bodyStream = renderToNodeStream(app);
      const transformStream = new HTMLTransform(); // this transfrom needed to do clever stream concatenation

      bodyStream.pipe(
        transformStream,
        { end: false },
      );
      bodyStream.on('end', () => {
        const htmlStates = {
          htmlStates,
          helmet: helmetContext.helmet,
        };
        // at this point you can see your helmetContext.helmet has filled 
        const [header, footer] = htmlTemplate(htmlStates);

        transformStream.write(header);
        transformStream.end(footer);
      });
      reply.send(transformStream);

As you can see we pipe body, then write head and footer because we need to process body first. That’s why we need to use transform stream to flip it back to proper order. Here is simple implementation for HTMLTransform.

class HTMLTransform extends Transform {
  constructor(options) {
    super(options);

    this.bufferedChunks = [];
    this.redis = options.redis;
    this.cacheKey = options.cacheKey;
  }

  _transform(data, enc, cb) {
    this.bufferedChunks.push(data);

    if (this.bufferedChunks.length === 1) {
      // first chunk should not be passed to the stream
      return cb(null);
    }

    if (this.bufferedChunks.length === 2) {
      const [body, head] = this.bufferedChunks;

      // 2nd chunk should be swapped and write to the stream
      return cb(null, Buffer.concat([head, body]));
    }

    // the rest should be just normal callback
    return cb(null, data);
  }

  // flush() is called when everything is done
  _flush(cb) {
    // if you want to cache the HTML do it here
    cb();
  }
};

Just checking if there were any updates on this, I am currently having a similar issue. Any of my helmet tags are always empty.

<title data-rh="true"></title>

Looks like the main contributor of this app isn’t really active… thanks for putting this out here… It would be great if you engaged with some of your followers here @staylor

I am having the exact same issue, working from the minimal example on React 16.8.2 and node 9.6. This happens every time I try to use it

@oyeanuj are you streaming? Check out #3

@staylor I noticed that what seems to be happening is that the render method of the child component in the tree which has the meta tags hasn’t been invoked in time for the call to helmet.meta.toString() above.

Do you think race conditions are a possible reason? Or is there another way to structure it to ensure that RHA has gone thru the tree and traversed the render of all child components?