Recently I've been commissioned to build a lawyer's website, to which I decided to develop using Gatsby for its Static Site Generation capability and its GraphQL API. As it is a business site, it needed to be translated to several languages - that's where I met trouble.

At this point, I had a Layout component that I was using in every page component as a Wrapper, that was being rerendered every time the user navigated to another page. This caused some visual glitches, more specifically on some animations of the mobile navigation bar (a child of the Layout component).

const IndexPage = () => {
  return (
    <Layout>
      // Other stuff here
    </Layout>
  );
}
Every page component was like this, unmanageable.

Gatsby provides certain APIs to customize building a site. I became interested in wrapPageElement, which is a function available in both Server Rendering and Browser APIs. This function lets you wrap pages in a component of your choice - in my case, it let me wrap my pages in the Layout component. But, of course, things are never this easy.

// gatsby-ssr.js and gatsby-browser.js
const React = require('react');
const Layout = require('./src/components/Layout').default;

exports.wrapPageElement = ({ element }) => {
  return <Layout>{element}</Layout>
};
What could go wrong?

This site I'm building uses gatsby-plugin-react-i18next to "easily translate your Gatsby website into multiple languages" (oh, the irony). As soon as I wrapped my pages that way, I lost the translations in the Layout component and its children, and started getting this error: warn react-i18next:: You will need to pass in an i18next instance by using initReactI18next.

I started investigating, first with some logs here and there. I noticed that the element that I was receiving as the element in wrapPageElement was of type I18nextProvider, and that piqued my interest. I went to look at this plugin's source code and discovered it has an implementation of wrapPageElement which returns wraps pages inside an i18next Context and respective provider.

const withI18next = (i18n: I18n, context: I18NextContext) => (children: any) => {
  return (
    <I18nextProvider i18n={i18n}>
      <I18nextContext.Provider value={context}>
      	{children} // Page element here
      </I18nextContext.Provider>
    </I18nextProvider>
  );
};

export const wrapPageElement = (
  {element, props}: WrapPageElementBrowserArgs<any, PageContext>,
  ...otherArgs
 ) => {
  // i18next magic...
  return withI18next(i18n, context)(element);
}
An excerpt of gatsby-plugin-react-i18next wrapper implementation

Now, I know why Layout and its children had lost their translations: they weren't able to access the Context provided by the plugin. The solution for this was clear to me then: I had to put my pages inside the Context instead of wrapping the Context in my pages!

// gatsby-ssr.js and gatsby-browser.js
const React = require('react');
const Layout = require('./src/components/Layout').default;

exports.wrapPageElement = ({ element }) => {
  const newElement = React.cloneElement(
    element,  // I18nextProvider
    element.props,
    React.cloneElement(
      element.props.children,  // I18nextContext.Provider
      element.props.children.props,
      React.createElement(
        Layout,
        undefined,
        element.props.children.props.children,
      ),
    ),
  );

  return newElement;
};

As a final note, do not forget that you have to use the wrapPageElement in both SSR and Browser APIs to achieve the same result server and client-side!