Coder Social home page Coder Social logo

react-helmet-async's Introduction

react-helmet-async

CircleCI

Announcement post on Times Open blog

This package is a fork of React Helmet. <Helmet> usage is synonymous, but server and client now requires <HelmetProvider> to encapsulate state per request.

react-helmet relies on react-side-effect, which is not thread-safe. If you are doing anything asynchronous on the server, you need Helmet to encapsulate data on a per-request basis, this package does just that.

Usage

New is 1.0.0: No more default export! import { Helmet } from 'react-helmet-async'

The main way that this package differs from react-helmet is that it requires using a Provider to encapsulate Helmet state for your React tree. If you use libraries like Redux or Apollo, you are already familiar with this paradigm:

import React from 'react';
import ReactDOM from 'react-dom';
import { Helmet, HelmetProvider } from 'react-helmet-async';

const app = (
  <HelmetProvider>
    <App>
      <Helmet>
        <title>Hello World</title>
        <link rel="canonical" href="https://www.tacobell.com/" />
      </Helmet>
      <h1>Hello World</h1>
    </App>
  </HelmetProvider>
);

ReactDOM.hydrate(
  app,
  document.getElementById(‘app’)
);

On the server, we will no longer use static methods to extract state. react-side-effect exposed a .rewind() method, which Helmet used when calling Helmet.renderStatic(). Instead, we are going to pass a context prop to HelmetProvider, which will hold our state specific to each request.

import React from 'react';
import { renderToString } from 'react-dom/server';
import { Helmet, HelmetProvider } from 'react-helmet-async';

const helmetContext = {};

const app = (
  <HelmetProvider context={helmetContext}>
    <App>
      <Helmet>
        <title>Hello World</title>
        <link rel="canonical" href="https://www.tacobell.com/" />
      </Helmet>
      <h1>Hello World</h1>
    </App>
  </HelmetProvider>
);

const html = renderToString(app);

const { helmet } = helmetContext;

// helmet.title.toString() etc…

Streams

This package only works with streaming if your <head> data is output outside of renderToNodeStream(). This is possible if your data hydration method already parses your React tree. Example:

import through from 'through';
import { renderToNodeStream } from 'react-dom/server';
import { getDataFromTree } from 'react-apollo';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import template from 'server/template';

const helmetContext = {};

const app = (
  <HelmetProvider context={helmetContext}>
    <App>
      <Helmet>
        <title>Hello World</title>
        <link rel="canonical" href="https://www.tacobell.com/" />
      </Helmet>
      <h1>Hello World</h1>
    </App>
  </HelmetProvider>
);

await getDataFromTree(app);

const [header, footer] = template({
  helmet: helmetContext.helmet,
});

res.status(200);
res.write(header);
renderToNodeStream(app)
  .pipe(
    through(
      function write(data) {
        this.queue(data);
      },
      function end() {
        this.queue(footer);
        this.queue(null);
      }
    )
  )
  .pipe(res);

Usage in Jest

While testing in using jest, if there is a need to emulate SSR, the following string is required to have the test behave the way they are expected to.

import { HelmetProvider } from 'react-helmet-async';

HelmetProvider.canUseDOM = false;

Prioritizing tags for SEO

It is understood that in some cases for SEO, certain tags should appear earlier in the HEAD. Using the prioritizeSeoTags flag on any <Helmet> component allows the server render of react-helmet-async to expose a method for prioritizing relevant SEO tags.

In the component:

<Helmet prioritizeSeoTags>
  <title>A fancy webpage</title>
  <link rel="notImportant" href="https://www.chipotle.com" />
  <meta name="whatever" value="notImportant" />
  <link rel="canonical" href="https://www.tacobell.com" />
  <meta property="og:title" content="A very important title"/>
</Helmet>

In your server template:

<html>
  <head>
    ${helmet.title.toString()}
    ${helmet.priority.toString()}
    ${helmet.meta.toString()}
    ${helmet.link.toString()}
    ${helmet.script.toString()}
  </head>
  ...
</html>

Will result in:

<html>
  <head>
    <title>A fancy webpage</title>
    <meta property="og:title" content="A very important title"/>
    <link rel="canonical" href="https://www.tacobell.com" />
    <meta name="whatever" value="notImportant" />
    <link rel="notImportant" href="https://www.chipotle.com" />
  </head>
  ...
</html>

A list of prioritized tags and attributes can be found in constants.ts.

Usage without Context

You can optionally use <Helmet> outside a context by manually creating a stateful HelmetData instance, and passing that stateful object to each <Helmet> instance:

import React from 'react';
import { renderToString } from 'react-dom/server';
import { Helmet, HelmetProvider, HelmetData } from 'react-helmet-async';

const helmetData = new HelmetData({});

const app = (
    <App>
      <Helmet helmetData={helmetData}>
        <title>Hello World</title>
        <link rel="canonical" href="https://www.tacobell.com/" />
      </Helmet>
      <h1>Hello World</h1>
    </App>
);

const html = renderToString(app);

const { helmet } = helmetData.context;

License

Licensed under the Apache 2.0 License, Copyright © 2018 Scott Taylor

react-helmet-async's People

Contributors

andipaetzold avatar blittle avatar brentvatne avatar chenxsan avatar forabi avatar jakeginnivan avatar justinph avatar kitten avatar ldegen avatar lukaswiklund avatar matteo-hertel avatar mharj avatar michenly avatar mnigh avatar mxstbr avatar nminhnguyen avatar sergei-zelinsky avatar sleepycat avatar slorber avatar staylor avatar visormatt avatar vxna avatar wojtekmaj avatar woodb avatar yokinist avatar yowakita avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-helmet-async's Issues

Error: Maximum call stack exceeded

I ran into a problem with react@16 and react-helmet-async. In version 16 every children contains cyclic property ._owner.alternate.alternate.alternate.alternate.... You compare props with deep-equal so it causes an error "Maximum call stack exceeded".

image

Expand TypeScript definitions

index.d.ts offers very basic type information for this package, but misses some properties.

I am setting HelmetProvider.canUseDOM = false (see #41), but canUseDOM isn't defined.

For now, I am working around this by adding my own definitions on top:

declare module 'react-helmet-async' {
  namespace HelmetProvider {
    var canUseDOM: boolean;
  }
}

But this should be fixed at the source. You can add a namespace (as above) or just put a static property on the class which is already exported. Also as noted by #38, since this depends on @types/react-helmet, that should be listed as a dependency of this project.

Please avoid `Object.assign`

Hi @staylor,

I run in problems in "good" old IE11, which does not implement Object.assign.

You use it here:

const props = Object.assign({}, instance.props);

We could replace this with an object spread op:

const props = {...instance.props};

Babeljs should pick this up and translate it into something that works even without Object.assign.
Alternatively, we could use the transform-object-assign-Plugin.

I am creating a PR for this, if that's ok for you?

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?

Is it possible to disable `data-rh`?

I'm using reactjs as server side template, no rehydrate needed on the client side. So I think data-rh is useless then, would love to disable it.

Tags within JSX fragments are ignored

JSX within fragments that are children of <Helmet> are ignored as of 1.0.4 and on React 16.10. I don't believe this was always the case.

Exmple helper that I had used to add route-specific tags:

const siteTitle = 'Example Site'
const prependSite = str => `${siteTitle} | ${str}`

const MetaTags = ({ description, image, title }) => (
  <Helmet>
    {
      description &&
      <>
        <meta property="og:description" content={description} />
        <meta name="twitter:title" content={description} />
      </>
    }
    {
      title &&
      <>
        <title>{prependSite(title)}</title>
        <meta property="og:title" content={prependSite(title)} />
        <meta name="twitter:title" content={prependSite(title)} />
      </>
    }
    {
      image &&
      <>
        <meta property="og:image" content={image} />
        <meta name="twitter:image" content={image} />
      </>
    }
  </Helmet>
)

This no longer adds any tags, and needs to be revised to:

const siteTitle = 'Example Site'
const prependSite = str => `${siteTitle} | ${str}`

const MetaTags = ({ description, image, title }) => (
  <Helmet>
    {description && <meta property="og:description" content={description} />}
    {description && <meta name="twitter:title" content={description} />}
    {title && <title>{prependSite(title)}</title>}
    {title && <meta property="og:title" content={prependSite(title)} />}
    {title && <meta name="twitter:title" content={prependSite(title)} />}
    {image && <meta property="og:image" content={image} />}
    {image && <meta name="twitter:image" content={image} />}
  </Helmet>
)

If this is expected behavior, it feels worth a warning.

Possibly related: nfl/react-helmet#342

API for notifying header complete during streaming

EDIT: closing because signaling "head is ready" is not a concern for helmet and depends on the SSR implementation. #3 (comment)

Since Helmet changes the header, you don't really know when the header can be sent until the entire app was rendered. In your streaming example, this happens during the getDataFromTree call, but since headers may change depending on graphql results, that won't be correct.

If instead there was a way to mark when headers are ready to send, the streaming render could be buffered until the signal is given, at which point the headers are extracted and the buffered body is piped. Once the body is rendered, extract and send the footer.

For example, <Helmet headComplete .../> could be used, which might trigger a callback that was passed to the provider:

const ctx = {headDone: () => {}}
// We use a const function to refer to the mutating headDone
const handleHead = () => ctx.headDone()
const app = (
	<HelmetProvider context={ctx} onHeadReady={handleHead}>
	  ...<Helmet headComplete .../>
	</HelmetProvider>
)
await getDataFromTree(app);
const waitForHead = new Promise(resolve => {ctx.headDone = resolve});

let didPause = false;
let resume
renderToNodeStream(app)
  .pipe(
    through(
      function write(data) {
		if (!didPause) {didPause=true; this.pause(); resume = this.resume.bind(this)}
        this.queue(data);
      },
      function end() {
        this.queue(getFooterFromContext(ctx));
        this.queue(null);
      }
    )
  )
  .pipe(res);

await waitForHead;
res.status(200);
res.write(getHeaderFromContext(ctx));
if (resume) resume();
didPause = true;

Something like this maybe? It needs some edge cases covered, but you get the jist…

Ability to have granular control over order of elements

This is also an issue (but not a GitHub one...) in react-helmet.

While this library, and react-helmet, allow some level of control over the order of head elements - by element type: link, meta, etc - it would be good to have fine-grain control over what element goes where as the position of elements inside the head is important.

An example of the benefits of this are in meta elements, where it is best for some key meta elements to come at the top of the head, i.e. charset, and some meta elements to come towards the bottom, i.e. open-graph data.

typescript typings esModuleInterop

this is a non standard flag, everybody who uses the standard tsconfig will get a compilation error.

TS1259: Module '"/node_modules/@types/react/index"' can only be default-imported using the 'esModuleInterop' flag

Error using library with Storybook

I'm not sure if this related to the fact that storybook/components uses react-helmet-async, but I'm seeing the error "Cannot read property 'add' of undefined" on hot-reloads only in storybooks.

My code looks like this:

<Provider store={store}>
    <HelmetProvider context={helmetContext}>
        <MainRoute />
    </HelmetProvider>
</Provider>,

Yet another rAF/cAF shim in client.js

Hey, many thanks for the nice project.

react-helmet-async carries requestAnimationFrame shim on board. This is just the 7th rAF shim in our projects bundle. It's pretty safe to use the API directly in 2018 (yep, we'd also worred about rAF shiming to support old browsers in 2014).

Get tags as is

Is it possible to get tags like plain string instead toString() or toComponent()?
Something like this helmet.title.toRaw() or helmet.title.toSrc()

Possible bug with FilledContext type

I've been researching into this for a day or two now, and believe the type for FilledContext needs to have the helmet property as optional like so:

export type FilledContext = {
  helmet?: HelmetData;
};

I believe this because if you follow the instructions in the Usage section of the README, we create an empty helmetContext constant, however the helmet property is currently marked as required. So when you try to destructure helmet from helmetContext, TS says there is no helmet property (as declared earlier).

import React from 'react';
import { renderToString } from 'react-dom/server';
import Helmet, { HelmetProvider } from 'react-helmet-async';

const helmetContext = {};

const app = (
  <HelmetProvider context={helmetContext}>
    <App>
      <Helmet>
        <title>Hello World</title>
        <link rel="canonical" href="https://www.tacobell.com/" />
      </Helmet>
      <h1>Hello World</h1>
    </App>
  </HelmetProvider>
);

const html = renderToString(app);

const { helmet } = helmetContext;

// helmet.title.toString() etc…

I'd love to create a PR for this, if this is in fact a correct fix.

Thanks! 😄

TypeScript: Cannot find module 'react-helmet'

✖ 「atl」: Checking finished with 1 errors
[at-loader] ./node_modules/react-helmet-async/index.d.ts:2:38
    TS2307: Cannot find module 'react-helmet'.

The TS definition seems to require react-helmet even though it’s not a dependency or peer dep. Any way we can use this package without having to install react-helmet?

Following example, adding a title tag like example is always undefined

      const components = (
        <StyleSheetManager sheet={sheet.instance}>
          <HelmetProvider context={helmetContext}>
            <ApolloProvider store={ctx.store} client={ctx.apollo.client}>
              <StaticRouter location={ctx.request.url} context={routeContext}>
                <Helmet>
                  <title>Test</title>
                </Helmet>
              </StaticRouter>
            </ApolloProvider>
          </HelmetProvider>
        </StyleSheetManager>
      )
      const jsx = sheet.collectStyles(components)
      await getDataFromTree(components)
      if ([301, 302].includes(routeContext.status)) {
        ctx.status = routeContext.status
        ctx.redirect(routeContext.url)
        return
      }
      if (routeContext.status === 404) {
        if (config.handler404) {
          config.handler404(ctx)
          return
        }
        ctx.status = routeContext.status
      }
      const htmlStream = new PassThrough()
      htmlStream.write('<!DOCTYPE html>')
      console.log(helmetContext.helmet.title.toString()) //outputs <title data-rh="true"></title>

Request: Ability to override `canUseDOM` when constructing a provider

I'd be happy to put up a PR if this makes sense and sounds like something you'd allow.

Details of my use case:

In the browser, when we preload a client-side navigation, we essentially create a second React app with its own providers to ensure that we've loaded everything (graphql data & chunks) we need for the next navigation. In some cases, we just use the same context as the main web app (like Apollo client's provider, since we want to get all the prefetched data cached), but in other cases (like Helmet), we definitely don't want the same context because we don't want to update the page's metadata until later after the prefetching when we actually do the navigation.

Does not display the title when switching between pages

Hello!
I have a static site with ssr. Ssr tuned and working fine for me.

Fast example:

Into my main component i put this code:

import React, { Component } from 'react';
import { Helmet, HelmetProvider } from 'react-helmet-async'
import { Route, Switch } from 'react-router-dom';
import routes from './routes';

export default class App extends Component {
  const helmetContext = {}
  
  render() {
    return (
		<>
			<HelmetProvider context={ helmetContext }>
				<Helmet titleTemplate="My site - %s" defaultTitle="Main page">
					<html lang="en" />
					<meta charSet="utf-8" />
				</Helmet>
			</HelmetProvider>
			
			<Switch>
			  {routes.map(route => (
				<Route
				  key={route.path}
				  path={route.path}
				  exact={route.exact}
				  component={route.component}
				/>
			  ))}
			</Switch>
			
		</>
	);
  }
}

Into other pages i put it example:

import React from 'react';
import { Helmet, HelmetProvider } from 'react-helmet-async'

export default const PageOne = () => {
	return(
		<div>
			<HelmetProvider>
				<Helmet
				  title="Page 1"
				  meta={[
					{
					  name: 'description',
					  content: 'Description of Page 1',
					},
				  ]}
				/>
			</HelmetProvider>
			Page 1 content
		</div>
	)
}

And two page:

import React from 'react';
import { Helmet, HelmetProvider } from 'react-helmet-async'

export default const PageTwo = () => {
	return(
		<div>
			<HelmetProvider>
				<Helmet
				  title="Page 2"
				  meta={[
					{
					  name: 'description',
					  content: 'Description of Page 2',
					},
				  ]}
				/>
			</HelmetProvider>
			Page 1 content
		</div>
	)
}

If load each of pages as separate the title is put. But if you switch between them through tag Link, then the title remains old.

I am doing something wrong. Thank you for any advice. Thanks.

version 0.2 is not compatible with react-router

I have tried version 0.2 from alpha to beta.1 but I can see always next issue related somehow to react-router.

Uncaught (in promise) TypeError: Cannot read property 'router' of undefined
    at ProxyComponent.getChildContext (app.js:127634)
    at ProxyComponent.getChildContext (app.js:123532)

React 16.6.0 bug

There seems to be an issue with react 16.6.0 -1 context api on server side render

getting following errors during render

Warning: Rendering <Context.Consumer.Provider> is not supported and will be removed in a future major release. Did you mean to render <Context.Provider> instead?
Warning: Rendering <Context.Consumer.Consumer> is not supported and will be removed in a future major release. Did you mean to render <Context.Consumer> instead?
Warning: Failed prop type: The prop `context` is marked as required in `Dispatcher`, but its value is `undefined`.
    in Dispatcher

TypeError: Cannot read property 'helmetInstances' of undefined

  • Dispatcher.js:70 Dispatcher.init
    [id-portal-web]/[react-helmet-async]/lib/Dispatcher.js:70:48

  • Dispatcher.js:116 Dispatcher.render
    [id-portal-web]/[react-helmet-async]/lib/Dispatcher.js:116:12

this seems to be happening because  of context consumer is initialized on first run with null values 

return _react2.default.createElement(
_Provider.Context.Consumer,
null,
function (context) {
// 👇 here
return _react2.default.createElement(_Dispatcher2.default, _extends({}, newProps, { context: context }));
}
);


I had similar issues on other Context using packages with server sider render. 

Does it work for Next.js?

I'm struggling to understand how this would be implemented for Next.js. Is there an example that you can point me to?

react-helmet-async SSR using apollo-client 2.3

Hey, I am trying to get react-helmet-sync to work with Apollo. I found this awesome post that made me think react-helmet-sync would be a better choice since I switched to Apollo from Relay.

I am definitely missing something here. I am not getting a value for console.log("title?: ", helmet.title.toString())

server.js

import ApolloClient from 'apollo-client'
import { ApolloProvider, renderToStringWithData } from 'react-apollo'
import Html from './routes/Html'
import App from './routes/App'
import Helmet, { HelmetProvider } from 'react-helmet-async';
  const client = new ApolloClient({
    ssrMode: true,
    link: ApolloLink.from(links),
    cache: new InMemoryCache(),
  })
  const context = {}
  const helmetContext = {};
  const component = (
    <HelmetProvider context={helmetContext}>
    <ApolloProvider client={client}>
      <StaticRouter location={req.url} context={context}>
        <App />
      </StaticRouter>
    </ApolloProvider>
    </HelmetProvider>
  )

  renderToStringWithData(component)
    .then(content => {
      res.status(200)
      const { helmet } = helmetContext;
      console.log("title?: ", helmet.title.toString())
      const html = <Html content={ content } client={ client } helmet={helmet}/>
      
      res.send(`<!doctype html>\n${ ReactDOM.renderToStaticMarkup(html) }`)
      res.end()
    })
    .catch(e => {
      console.log(e)
      const content = ReactDOM.renderToStaticMarkup(
        <div id='main-container'>
          <ErrorScreen message='Internal Server Error' />
        </div>)
      const html = <Html content={ content } client={ client }  />

      res.status(500)
      res.send(`<!doctype html>\n${ ReactDOM.renderToStaticMarkup(html) }`)
      res.end()
    })
})

client.js

const client = new ApolloClient({
  ssrForceFetchDelay: 100,
  link: ApolloLink.from(links),
  connectToDevTools: true,
  // here we're initializing the cache with the data from the server's cache
  cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
})

hydrate(
  <ErrorBoundary component={props =>
    <div id='main-container'>
      <ErrorScreen {...props} message='Internal Server Error' />
    </div>
  }>
  <HelmetProvider >
    <ApolloProvider client={client}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </ApolloProvider>
    </HelmetProvider>,
  </ErrorBoundary>,
  document.getElementById('content')
)

Thanks for your help.

Extract data on client

I have to extract data on client (like peek() on react-helmet), how can I do this with react-helmet-async?

helmetContext working only for non-DOM environment

Incorrect dist-tag for react-helmet-async@latest

Just wanted to let you know that the latest dist-tag for react-helmet-async is currently incorrect. It points to 0.2.0-beta.1, which it should probably point to 0.1.0. This will result in folks upgrading to the beta version automatically, which they should probably stay on the 0.1.x branch.

Best practises for testing?

I was wondering what people did for testing the meta tags with Jest.

Currently, I have a test like so:

    test('renders meta tags', () => {
        const context = {}
        HelmetProvider.canUseDOM = false
        customRender(
            <HelmetProvider context={context}>
                <Login />
            </HelmetProvider>,
        )
        const head = (context as any).helmet
        Object.values(head).forEach((comp: any) => {
            expect(comp.toString()).toMatchSnapshot()
        })
    })

It would be nice to use inline snapshots as it's easier to read. Also, it seems like a lot of boilerplate.

Two downsides with this approach are:

It is not prettified so a component rendering many meta elements will land on one line, and secondly, the snapshot names are not so readable (what is meta tag 5?).

exports[`<Login> meta tag 5`] = `"<meta data-rh=\\"true\\" name=\\"description\\" /><meta data-rh=\\"true\\" name=\\"image\\" /><meta data-rh=\\"true\\" name=\\"twitter:card\\" content=\\"summary\\"/><meta data-rh=\\"true\\" name=\\"twitter:title\\" /><meta data-rh=\\"true\\" name=\\"twitter:description\\" /><meta data-rh=\\"true\\" name=\\"twitter:image:src\\" /><meta data-rh=\\"true\\" name=\\"twitter:image\\" /><meta data-rh=\\"true\\" property=\\"og:title\\" />"`;

alpha only error

Hi,

I just installed the alpha version 0.1.0-alpha (latest tag), but I got:

: Uncaught TypeError: Cannot read property 'add' of undefined

It works well with 0.1.0 though.

I appreciate any help 🙏

Uncaught TypeError: Cannot read property 'add' of undefined

helmetInstances is supposed to be in context but your readme does not indicate any other set up steps. Do you know what is missing from the documentation that might cause this error?

    key: 'componentWillMount',
    value: function componentWillMount() {
      var helmetInstances = this.context.helmetInstances;

--->  helmetInstances.add(this);
      this.emitChange();
    }

Cannot read property 'add' of undefined
at Dispatcher.componentWillMount (Dispatcher.js:73)
at callComponentWillMount (react-dom.development.js:6370)
at mountClassInstance (react-dom.development.js:6435)
at updateClassComponent (react-dom.development.js:7840)
at beginWork (react-dom.development.js:8225)
at performUnitOfWork (react-dom.development.js:10224)
at workLoop (react-dom.development.js:10288)
at HTMLUnknownElement.callCallback (react-dom.development.js:542)
at Object.invokeGuardedCallbackDev (react-dom.development.js:581)
at invokeGuardedCallback (react-dom.development.js:438)

react Js

I am trying to start npm I get this
./node_modules/postcss-preset-env/index.mjs
Can't import the named export 'feature' from non EcmaScript module (only default export is available)

Helmet context is always empty when head is rendered

Following the example, awaiting my dataFromTree, the helmet is always empty. Every time, no matter how trivial the data, the helmet components.toString are always undefined.
The following is based on Koa 2.0

export function createReactHandler(css = [], scripts = [], chunkManifest = {}) {
  return async function reactHandler(ctx) {
    const sheet = new ServerStyleSheet()
    try {
      const routeContext = {}
      const components = (
        <StyleSheetManager sheet={sheet.instance}>
          <ApolloProvider store={ctx.store} client={ctx.apollo.client}>
            <HelmetProvider context={helmetContext}>
              <StaticRouter location={ctx.request.url} context={routeContext}>
                <App />
              </StaticRouter>
            </HelmetProvider>
          </ApolloProvider>
        </StyleSheetManager>
      )
      const state = ctx.store.getState()
      const frontend = sheet.collectStyles(components)
      await getDataFromTree(components)

      if ([301, 302].includes(routeContext.status)) {
        ctx.status = routeContext.status
        ctx.redirect(routeContext.url)
        return
      }
      if (routeContext.status === 404) {
        if (config.handler404) {
          config.handler404(ctx)
          return
        }
        ctx.status = routeContext.status
      }
      const combinedStream = CombinedStream.create()
      const {helmet} = helmetContext
      combinedStream.append(stringToStream(`<!DOCTYPE html>${getHeader({css,
        state,
        scripts,
        chunkManifest,
        window: {webpackManifest: chunkManifest},
        metaTags:
          helmet.title.toString() +
          helmet.meta.toString() +
          helmet.link.toString()})}`))
      const stream = sheet.interleaveWithNodeStream(renderToNodeStream(frontend))
      combinedStream.append(stream)
      combinedStream.append(stringToStream(getFooter({scripts, state})))
      ctx.type = 'text/html'
      ctx.body = combinedStream
    } finally {
      Object.seal(sheet) // https://github.com/styled-components/styled-components/issues/1624 mem leak on 404
    }
  }
}

getTagsFromPropsList behavior depends on Object.keys order of tags

Because the primaryAttributeKey is determined by side effects while looping over Object.keys(tag), it matters what order those keys are generated in, which I don't think is going to be the expected behavior. For example, in this unit test, if on line 366 href: null is moved to the front of the object, when I run the test it no longer passes.

Unfortunately I am not quite familiar enough yet with this library or the specifics of how all the <head> tags work to make the decisions needed for a PR that would address this problem (e.g. in the case of link since rel and href are both "primary" should both be stored during the loop, one as the deterministically chosen primary and the other just to check for null values, per that unit test? Not sure....)

I can see this code was inherited from react-helmet but am not sure if there's any benefit trying to engage the original authors there given it doesn't look to be actively maintained anymore.

For context, I came across this issue as an obstacle when trying to work on a Pull Request that introduces a new custom attribute which when attached to script tags lets the most deeply nested one overwrite others with the same attribute, similar to the existing behavior for other tags. This is motivated by wanting to work with the same semantics in <Helmet> that say the <meta> tag gives for Opengraph properties, in the context of the <script> tag for Google's structured data. I would be happy to submit PRs both to fix the blocking issue and also to introduce this feature, if some decision can be made about what approach to take with the former, and if the latter would be a welcome addition.

Uncaught TypeError: Object prototype may only be an Object or null: undefined

I am using Parcel as a bundler and when I update to the latest version of react-helmet-async (1.0.1), I get an error on loading the page "Uncaught TypeError: Object prototype may only be an Object or null: undefined", in node_modules/react-helmet-async/lib/index.mjs.react-fast-compare (Provider.js:20). The Chrome debugger gives me this error on the last line of this piece of code in Provider.js (setting the titleAttributes property):

props.context.helmet = mapStateOnServer({
        baseTag: [],
        bodyAttributes: {},
        encodeSpecialCharacters: true,
        htmlAttributes: {},
        linkTags: [],
        metaTags: [],
        noscriptTags: [],
        scriptTags: [],
        styleTags: [],
        title: '',
        titleAttributes: {},
      });

Does anybody have an idea what this could be?

documentation - template example

Can you put an example of what this template dependency is to look like in the streaming example please, you reference it in code, however I do not see it.

Compatibiliy with Jest

Hi! Does anyone has success with this library and Jest? I replaced Helmet with Helmet-Async to avoid that bug with infinite recursion... Now I got some incompatibility with Jest (which I didn't have back then with original Helmet).

console.error node_modules/jsdom/lib/jsdom/virtual-console.js:29
      Error: Uncaught [TypeError: Cannot read property 'add' of undefined]
          at reportException (/Users/username/Projects/projectname/node_modules/jsdom/lib/jsdom/living/helpers/runtime-script-errors.js:66:24)
          at invokeEventListeners (/Users/username/Projects/projectname/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:209:9)
          at HTMLUnknownElementImpl._dispatch (/Users/username/Projects/projectname/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:119:9)
          at HTMLUnknownElementImpl.dispatchEvent (/Users/username/Projects/projectname/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:82:17)
          at HTMLUnknownElementImpl.dispatchEvent (/Users/username/Projects/projectname/node_modules/jsdom/lib/jsdom/living/nodes/HTMLElement-impl.js:30:27)
          at HTMLUnknownElement.dispatchEvent (/Users/username/Projects/projectname/node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:157:21)
          at Object.invokeGuardedCallbackDev (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:199:16)
          at invokeGuardedCallback (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:256:31)
          at replayUnitOfWork (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:17228:5)
          at renderRoot (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:18100:13) TypeError: Cannot read property 'add' of undefined
          at Dispatcher.init (/Users/username/Projects/projectname/node_modules/react-helmet-async/lib/Dispatcher.js:72:23)
          at Dispatcher.render (/Users/username/Projects/projectname/node_modules/react-helmet-async/lib/Dispatcher.js:116:12)
          at finishClassComponent (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:14418:31)
          at updateClassComponent (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:14381:10)
          at beginWork (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:15203:16)
          at performUnitOfWork (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:17941:12)
          at workLoop (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:17981:24)
          at callCallback (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:149:14)
          at batch (/Users/username/Projects/projectname/node_modules/react-easy-state/dist/cjs.es6.js:36:15)
          at HTMLUnknownElement.batched (/Users/username/Projects/projectname/node_modules/react-easy-state/dist/cjs.es6.js:55:28)
          at invokeEventListeners (/Users/username/Projects/projectname/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:193:27)
          at HTMLUnknownElementImpl._dispatch (/Users/username/Projects/projectname/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:119:9)
          at HTMLUnknownElementImpl.dispatchEvent (/Users/username/Projects/projectname/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:82:17)
          at HTMLUnknownElementImpl.dispatchEvent (/Users/username/Projects/projectname/node_modules/jsdom/lib/jsdom/living/nodes/HTMLElement-impl.js:30:27)
          at HTMLUnknownElement.dispatchEvent (/Users/username/Projects/projectname/node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:157:21)
          at Object.invokeGuardedCallbackDev (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:199:16)
          at invokeGuardedCallback (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:256:31)
          at replayUnitOfWork (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:17228:5)
          at renderRoot (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:18100:13)
          at performWorkOnRoot (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:18958:7)
          at performWork (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:18870:7)
          at performSyncWork (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:18844:3)
          at requestWork (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:18713:5)
          at scheduleWork (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:18522:5)
          at dispatchAction (/Users/username/Projects/projectname/node_modules/react-dom/cjs/react-dom.development.js:12122:5)
          at updater (/Users/username/Projects/projectname/node_modules/react-easy-state/dist/cjs.es6.js:141:31)
          at runTask (/Users/username/Projects/projectname/node_modules/react-easy-state/dist/cjs.es6.js:28:3)
          at Set.forEach (<anonymous>)
          at batch (/Users/username/Projects/projectname/node_modules/react-easy-state/dist/cjs.es6.js:40:13)
          at batched (/Users/username/Projects/projectname/node_modules/react-easy-state/dist/cjs.es6.js:55:28)
          at process._tickCallback (internal/process/next_tick.js:68:7)

I now it's an ugly chunk of code but if you inspect it, you see that it's caused by react-helmet-async.
It happens only in Jest, no problems in browser. Use-case has nothing special:

function Layout(props) {
  ...
  return [
      document && <Helmet key="0">
        <meta charSet="utf-8" />
        <title>{document.seoTitle}</title>
        <meta name="description" content={document.seoDescription}/>
        <meta property="og:type" content={document.ogType}/>
        <meta property="og:image" content={document.ogImage}/>
      </Helmet>,
  ...

if I remove <Helmet>...</Helmet>, the above problem disappears. I didn't forget to add provider like this:

export default function App (props) {
  return <HelmetProvider context={helmetContext}>
    <Router>

Do Fragments work?

This package seems to have the same issue as the original react-helmet, insomuch that it doesn't see <Fragment> as a valid tag and the check on line 143 in index.js sees the <Fragment> as a function (given it's actually a proxy, AFAICT?).

I may be totally wrong about this however and would absolutely love to be proven wrong. 😄

Relaxed versions?

Would it be possible to use ^ instead of specific versions so it's less likely that multiple versions of dependencies needing to be installed? For example, @babel/runtime is 7.3.4 so people using newer versions of babel will end up having multiple versions of the runtime installed

Confused: Seems to work fine even when head is output as part of `rendeToNodeStream`

Hi!

I just converted my SSR app to use renderToNodeStream and according to some of my Apache Bench benchmarks, it seems to be working (all requests are completed in comparison with renderToString which would block and cause the app to eventually crash).

The section about renderToNodeString in the README says that it can only be used with this lib if the head is output outisde renderToNodeString. However, in my case the <head> is output by renderToNodeString and everything seems to be working fine.

Now, maybe something is wrong and I haven't noticed, but I have tested changing the head from sub-components (the title) and it worked well. However, I'm afraid I'm breaking something I don't fully understand.

SSR in my app is implemented by a middleware called serverRenderer, here are the relevant contents:

import * as React from 'react';
import * as express from 'express';
import { renderToNodeStream } from 'react-dom/server';
import { StaticRouter as Router, matchPath } from 'react-router-dom';
import App from '../../shared/App';
import Html from '../components/HTML';
import { ServerStyleSheets, ThemeProvider } from '@material-ui/styles';
import theme from '../../shared/mui-theme';
import { HelmetProvider } from 'react-helmet-async';
import { getSnapshot } from 'mobx-state-tree';
import { getDataFromTree } from 'mst-gql';
import { RootStoreType, StoreContext } from '../../shared/models';
import { initializeStore } from '../../shared/utils/initModels';
import IntlProvider from '../../shared/i18n/IntlProvider';

const routerContext = {};
const helmetContext = {};

const serverRenderer: any = () => async (req: express.Request, res: express.Response, next: any) => {
  const match = ssrRoutes.find(route => matchPath(req.path, {
    path: route,
    exact: true
  }));

  const store = initializeStore(true, undefined, req.header('Cookie'));

  const comp =
    <StoreContext.Provider value={store}>
      <Router location={req.url} context={routerContext}>
        <IntlProvider>
          <HelmetProvider context={helmetContext}>
            <ThemeProvider theme={theme}>
              <App />
            </ThemeProvider>
          </HelmetProvider>
        </IntlProvider>
      </Router>
    </StoreContext.Provider>;

  const sheets = new ServerStyleSheets();
  const content = await getDataFromTree(sheets.collect(comp), store);
  const initialState = getSnapshot<RootStoreType>(store);
  const css = sheets.toString();

  res.write('<!doctype html>');
  const html =
    <Html
      css={[res.locals.assetPath('bundle.css'), res.locals.assetPath('vendor.css')]}
      helmetContext={helmetContext}
      scripts={[res.locals.assetPath('bundle.js'), res.locals.assetPath('vendor.js')]}
      inlineStyle={css}
      state={JSON.stringify(initialState)}
    >
      {content}
    </Html>;

    renderToNodeStream(html).pipe(res);
};

export default serverRenderer;

The HTML helper is the one that has the actual HTML structure, here's how it looks like

import React from 'react';
import Helmet from 'react-helmet';

interface IProps {
    children: any;
    css: string[];
    helmetContext: any;
    inlineStyle: string;
    scripts: string[];
    state: string;
}

const HTML = ({
    children,
    inlineStyle = '',
    css = [],
    helmetContext: { helmet },
    scripts = [],
    state = '{}'
}: IProps) => {
    const head = Helmet.renderStatic();
    return (
        <html lang=''>
            <head>
                <meta charSet='utf-8' />
                <meta name='viewport' content='width=device-width, initial-scale=1' />
                {helmet.base.toComponent()}
                {helmet.title.toComponent()}
                {helmet.meta.toComponent()}
                {helmet.link.toComponent()}
                {helmet.script.toComponent()}
                {css.filter(Boolean).map(href => (
                    <link key={href} rel='stylesheet' href={href} />
                ))}
                <style id='jss-server-side' dangerouslySetInnerHTML={{__html: inlineStyle}}></style>
                <script
                    // eslint-disable-next-line react/no-danger
                    dangerouslySetInnerHTML={{
                        // TODO: Add jsesc/stringify here
                        // see: https://twitter.com/HenrikJoreteg/status/1143953338284703744
                        __html: `window.__PRELOADED_STATE__ = ${state}`,
                    }}
                />
            </head>
            <body>
                {/* eslint-disable-next-line react/no-danger */}
                <div id='app' dangerouslySetInnerHTML={{ __html: children }} />
                {scripts.filter(Boolean).map(src => (
                    <script key={src} src={src} />
                ))}
            </body>
        </html>
    );
};

export default HTML;

And it (seems) to work well, requests are properly rendered server-side, head attributes are changed when using <Helmet> from components.

I'd really love some insights on why it works well if it's recommended against, perhaps you could elaborate a bit more about this?

Thanks!

License

README states MIT, package.json says Apache 2
I can open a PR, but I'm not sure which one is intended.

TypeError: Cannot read property 'add' of undefined

So I tested my code after #25 was merged. Unfortunately, the problem still exists because two different probjects are consuming two different Provider modules, i.e., two different Context exported in Provider. So that pull request didn't fix the real problem.

empty title tags when rendering component

Hello!

I'm trying to swap out my usage from react-helmet to use react-helmet-async, but I am noticing that my title tags are empty every single time.

Here's how what I'm doing it:

On the client, the component to be rendered.

        <HelmetProvider>
            <Helmet>
                <title>Hello</title>
            </Helmet>
        </HelmetProvider>

On the server

const withHelmetProvider = (Component, context) => ({ ...props }) => (
    <HelmetProvider context={context}>
        <Component {...props} />
    </HelmetProvider>
);

const helmetContext = {};
ReactDOM.renderToString(withHelmetProvider(Component, helmetContext));
const { helmet } = helmetContext;
console.log(helmet.title.toStringI());

I see that every time the server logs, the tag is empty:

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

What are some possible causes for this? I've verified the following:

  • The client and server are using the same version of react-helmet-async (^1.0.4)
  • The HelmetProvider is high enough, the component really just that!

I wanted to ask what could be some common causes for it?

I've made sure that the helmetContext is living for the entire lifecycle of the request.

Typescript types out of sync

The types of react-helmet have been updated and now there is no default export anymore. Check out this PR:
https://github.com/DefinitelyTyped/DefinitelyTyped/pull/38557/files

The PR updates also the @types/react-helmet-async package. But as it turns out, react-helmet-async includes its own types, which relies on the old version of react-helmet. (It imports the default export.) Therefore in the code an error is shown as JSX element type 'Helmet' does not have any construct or call signatures..

Why is there a d.ts file in this repo and in DefinitelyTyped? Should I file a PR for this repo?

Following example throws error

Following example

        <StyleSheetManager sheet={sheet.instance}>
          <ApolloProvider store={ctx.store} client={ctx.apollo.client}>
            <StaticRouter location={ctx.request.url} context={routeContext}>
              <HelmetProvider context={helmetContext}>
                <App>
                  <Helmet>
                    <title>test</title>
                    <link rel="canonical" href="https://test.com/" />
                  </Helmet>
                </App>
              </HelmetProvider>
            </StaticRouter>
          </ApolloProvider>
        </StyleSheetManager>
index.module.js?d4ce:1 Uncaught TypeError: Cannot read property 'add' of undefined
    at e.init (index.module.js?d4ce:1)
    at e.render (index.module.js?d4ce:1)
    at finishClassComponent (react-dom.development.js?cada:14695)
    at updateClassComponent (react-dom.development.js?cada:14650)
    at beginWork (react-dom.development.js?cada:15598)
    at performUnitOfWork (react-dom.development.js?cada:19266)
    at workLoop (react-dom.development.js?cada:19306)
    at HTMLUnknownElement.callCallback (react-dom.development.js?cada:149)
    at Object.invokeGuardedCallbackDev (react-dom.development.js?cada:199)
    at invokeGuardedCallback (react-dom.development.js?cada:256)

Enable unit testing by allowing use of context even if document exists

Right now the Provider class detects the presence of document to decide whether to update the document directly (browser mode) or the provided context object (server mode).

It is useful to be able to check helmet attributes during tests in an isolated way, where the tests might be running in a browser. Ideally this would not rely on the actual document title (which might be affected by another test), and would not rely on checking mocked Helmet properties (since multiple Helmets can override each other). A test could look like this:

it('sets the page title', () => {
  const context = {};
  const dom = mount((
    <HelmetProvider context={context}>
      <MyComponent />
    </HelmetProvider>
  ));
  expect(context.helmet.title.toString()).toEqual('My Title');
  dom.unmount();
});

But right now, context.helmet is null because it detected document.

I believe it would be fine to use the passed context if it is given, and fall-back to document if no context is given (removing the need for a document check entirely), but if that has problems, at least offering the ability to force using context via another property would help.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.