server-side-renderingreact-i18nextnext.js13next-i18nexthydration

NextJS+NextI18Next hydration error when trying to map through array: "Text content does not match server-rendered HTML"


I have a Next JS app with Next-i18next localization and SSR. It works fine in all cases except in one case where I have a custom carousel component on a page that takes in an array of objects as a prop and maps through said array to create the contents of the carousel. In order to take advantage of the i18next framework, I've put in the data as an array and use t('key', {returnObjects: true}) and map over the array returned. I am not sure why there's a disrepancy server- and client-side, doesn't the serverSideTranslations method of next-i18next load in the relevant data during said SSR?

I get the following errors:

Error: Text content does not match server-rendered HTML.

Warning: Text content did not match. Server: "" Client: "Correct defaultLocale string from the locale json"

See more info here: https://nextjs.org/docs/messages/react-hydration-error
Error: There was an error while hydrating. Because the error happened outside of a Suspense   
boundary, the entire root will switch to client rendering.

Relevant code:

pages/index.tsx

export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
    return {
        props: {
            ...(await serverSideTranslations(locale ?? 'ru', ['header', 'auth_modal', 'home'])),
        },
    };
};

export default function Home() {
    const { t } = useTranslation('home');

    return (
        <PageLayout title={title} description={description}>
            <BannerCarousel
                items={t('home:banner-carousel', { returnObjects: true })}
                speed={800}
            />

BannerCarousel.tsx

  {carouselItems.map((item, index) => {
                    return (
                        <BannerCarouselItem
                            key={item.title + index}
                            item={item}
                            speed={speed}
                            active={index === currentIndex}
                            transition={transitionEnabled}
                        />
                    );

Data type being passed into the carousel from public/locales

{
    "banner-carousel": [
        {
            "title": "title",
            "subtitle": "text",
            "imgUrl": "/main_carousel/action.jpg",
            "imgUrlMobile": "/main_carousel/action_mobile.jpg"
        },
  ]
}

I've tried gating the render of the carousel behind a useEffect with a boolean flag that updates once on the first render, I've tried loading in the array through a separate instance of i18n.t() as getServerSideProps props, nothing helps. What's so specific about mapping the return value of translation method that it breaks the SSR? I am not sure what to do in this situation, I am still new to NextJS and server work, maybe the problem is elsewhere entirely.


Solution

  • Make sure you import your useTranslation from the i18n Next.js library next-i18next instead of react-i18next. This is because the response of serverSideTranslations is a custom object with _nextI18Next property.

    After that, wrap around your entry component App with appWithTranslation higher-order component like so: appWithTranslation(App). This is so that whenever the static props (including _nextI18Next) is returned from the server-side, the client-side will then be able to figure out how to hydrate those data back into the React Context.


    Sample setup required in your code:

    App.tsx:

    import { appWithTranslation } from 'next-i18next';
    
    const App = () => {
      return <Home />;
    };
    export default appWithTranslation(App);
    

    Home.tsx:

    import { serverSideTranslations, useTranslation } from 'next-i18next';
    
    export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
      return {
        props: {
          ...(await serverSideTranslations(locale ?? 'ru', ['home'])),
        },
      };
    };
    
    const Home = () => {
      const { t } = useTranslation('home');
      return <h1>{t('home')}</h1>;
    };
    
    export default Home;