Next-i18next: Component using withNamespaces inside next/head crashes the app

Created on 1 Apr 2019  Â·  11Comments  Â·  Source: isaachinman/next-i18next

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.

Most helpful comment

@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.

All 11 comments

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

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} />.

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

@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.

@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

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?

@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.

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.

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

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"} />
    );
  }
}

@StarpTech #286.

Was this page helpful?
0 / 5 - 0 ratings