Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component using withNamespaces inside next/head crashes the app #251

Closed
Nelrohd opened this issue Apr 1, 2019 · 11 comments
Closed

Component using withNamespaces inside next/head crashes the app #251

Nelrohd opened this issue Apr 1, 2019 · 11 comments

Comments

@Nelrohd
Copy link

Nelrohd commented Apr 1, 2019

Describe the bug

Using a MyComponent with withNamespaces("common")(MyComponent) inside <Head> of next/head triggers an errors:

TypeError: Cannot read property 'wait' of null
    at NamespacesConsumerComponent.render (my-project/node_modules/react-i18next/dist/commonjs/NamespacesConsumer.js:213:33)

Occurs in next-i18next version

Any version.

Steps to reproduce

  1. Make a component using withNamespaces:
import React, { Component } from "react";

export const MetaOgTag = withNamespaces("common")(
  class extends Component {
    render() {
      const { title } = this.props;

      return (
        <>
          <meta property="og:title" content={t(title)} />
        </>
      )
    }
  }
);
  1. Use it in your pages/index.js inside <head>...</head>:
import React from "react";
import { MetaOgTag } from "../components/MetaOgTag";
import Head from "next/head";

export default class Index extends React.Component {

  render() {
    return (
      <Head>
        <MetaOgTag title={"Hello World"} />
      </Head>
    );
  }
}
  1. Reload your app and see the error.

Expected behaviour

It should not crash.

Screenshots

None.

OS (please complete the following information)

  • Macbook Pro 2017 13 inches
  • Browser: Google Chrome - Version 73.0.3683.86 (Official Build) (64-bit)

Additional context

It looks like it's not possible to use i18n inside head because something not ready/setup yet.

@isaachinman
Copy link
Contributor

Hi @Nelrohd - looks like that error is coming out of react-i18next. Most likely, req.i18n is still null. Did you debug any further?

@kachkaev
Copy link
Contributor

kachkaev commented Apr 1, 2019

Seems like i18n from React context is not available inside <Head />, so withNamespaces cannot get it. In one of my projects I just pass t as a prop and all works. When I need to reference a string in the common namespace I prefix the id with it, e.g. t("common:foobar").

Feel free to copy the component code (it's written in TypeScript).
import _ from "lodash";
import Head from "next/head";
import { useContext } from "react";
import { format, parse } from "url";
import { TFunction } from "../../i18n";
import { SsrUrlContext } from "../../lib/appContext";

interface Props {
  t: TFunction;
  title?: string | false;
  titleSuffix?: string | false;
  description?: string | false;
  keywords?: string | false;
  keywordsSuffix?: string | false;
  canonicalUrl?: string | false;
  supportedGetParamsInCanonicalUrl?: string[];
}

const PageMeta = ({
  t,
  title,
  titleSuffix,
  description,
  keywords,
  keywordsSuffix,
  canonicalUrl,
  supportedGetParamsInCanonicalUrl,
}: Props) => {
  const ssrUrl = useContext(SsrUrlContext);
  let derivedCanonicalUrl;
  if (canonicalUrl) {
    derivedCanonicalUrl = canonicalUrl;
  } else if (canonicalUrl !== false) {
    const { port, query, ...rest } = parse(
      typeof window !== "undefined" ? window.location.href : ssrUrl,
      true,
    );
    const cleanedQuery = _.pick(query, supportedGetParamsInCanonicalUrl || []);
    derivedCanonicalUrl = format({
      ...rest,
      hostname: undefined,
      host: undefined,
      protocol: undefined,
      search: undefined,
      query: _.fromPairs(_.orderBy(_.toPairs(cleanedQuery), (pair) => pair[0])),
      hash: undefined,
    }).replace("//", "");
  }

  const derivedTitleSuffix =
    titleSuffix !== false ? titleSuffix || t("common:pageTitleSuffix") : "";
  const derivedTitle = title !== false ? title || t("pageTitle") : null;

  const derivedDescription =
    description !== false ? description || t("pageDescription") : undefined;

  const derivedKeywordsSuffix =
    keywordsSuffix !== false && keywords !== false
      ? keywordsSuffix || t("common:pageKeywordsSuffix")
      : "";
  const derivedKeywords =
    keywords !== false ? keywords || t("pageKeywords") : null;

  return (
    <Head>
      <title>
        {derivedTitle}
        {derivedTitleSuffix}
      </title>
      <meta name="description" content={derivedDescription} />
      <meta
        name="keywords"
        content={`${derivedKeywords}${derivedKeywordsSuffix}`}
      />
      {derivedCanonicalUrl ? (
        <link rel="canonical" href={derivedCanonicalUrl} />
      ) : null}
    </Head>
  );
};

export default PageMeta;

On most pages you just use <PageMeta t={t} /> and the component will pick the right metadata as long as you page uses withNamespaces("pageName"). On the home page you might want to use <PageMeta t={t} titleSuffix={false} /> if you want to see My awesome website instead of Homepage – my awesome website. On a product / blog post page this would be something like <PageMeta t={t} title={product.title} description={product.description} />.

@isaachinman
Copy link
Contributor

That must mean that NextJs is putting the Head component somewhere outside our i18n provider.

@isaachinman
Copy link
Contributor

@Nelrohd I have a professional project wherein we're localising next/head inside our _app.tsx as such:

Head.tsx

import NextHead from 'next/head';
import * as React from 'react';

import { withNamespaces } from '../../i18n';

const Head = ({ t }) => (
  <NextHead>
    <title>{t('page.title')}</title>
  </NextHead>
);

export default withNamespaces('common')(Head);

_app.tsx

import * as React from 'react';

import NextApp, { Container } from 'next/app';
import { Head } from '../components';

import { appWithTranslation } from '../i18n';

class App extends NextApp {
  render() {
    const { Component, pageProps } = this.props;
    return (
      <Container>
        <Head />
        <Component {...pageProps} />
      </Container>
    );
  }
}

export default appWithTranslation(App);

Not sure exactly what is going wrong for you, but I can confirm that this approach works just fine.

@Nelrohd
Copy link
Author

Nelrohd commented Apr 3, 2019

@isaachinman Your example is different from mine.

In my example, my component use withNamespaces and is placed inside next/head.

In your example you create a component with withNamespaces and put its content inside next/head

@isaachinman
Copy link
Contributor

Yes I understand that. Clearly NextJs is lifting next/head into the head and out of the React tree. The approach I showed works - is it possible for you to refactor to this?

@Nelrohd
Copy link
Author

Nelrohd commented Apr 3, 2019

@isaachinman For sure, I already did it before in fact but I wanted to point the issue to help others and maybe have a fix if it's possible.

@isaachinman
Copy link
Contributor

I don't think there's going to be a possible fix outside of wrapping as I showed above. It's an acceptable solution and works just fine, though. Happy to continue discussion if anyone feels it's necessary.

@StarpTech
Copy link
Contributor

I also faced with that issue. This issue should be pinned in the readme 😄

@StarpTech
Copy link
Contributor

StarpTech commented Apr 29, 2019

Another solution is to wrap your tags in your component with a next/head component instead to centralize them in a single next/head at the end next will hoist them all together.

import React, { Component } from "react";
import Head from 'next/head';

export const MetaOgTag = withNamespaces("common")(
  class extends Component {
    render() {
      const { title } = this.props;

      return (
        <Head>
          <meta property="og:title" content={t(title)} />
        </Head>
      )
    }
  }
);
import React from "react";
import { MetaOgTag } from "../components/MetaOgTag";
import Head from "next/head";

export default class Index extends React.Component {

  render() {
    return (
      <Head>
        <title>Hello World</title>
      </Head>
      <MetaOgTag title={"Hello World"} />
    );
  }
}

@isaachinman
Copy link
Contributor

@StarpTech #286.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants