i18next / i18next-http-backend

i18next-http-backend is a backend layer for i18next using in Node.js, in the browser and for Deno.
MIT License
453 stars 70 forks source link

Hydration Error on page reload with next 12 + react 18 #91

Closed Christian-Schoenlein closed 2 years ago

Christian-Schoenlein commented 2 years ago

๐Ÿ› Bug Report

When using client side translation in development running next dev on page refresh react throws the following error: Error: Text content does not match server-rendered HTML.

The following errors appear in the console: Text content did not match. Server: "Hello World" Client: "hello_world"

171315322-998f31e7-ca87-4e4d-985d-8a07cf8ea809

Error message running production server next build and next start.

171317005-ad37d45c-14db-47a3-a422-a8c85d6be650

To Reproduce

The bug can be reproduced with the official next example and react & react-dom version 18.1.0.

Expected behavior

No hydration error.

Your Environment

adrai commented 2 years ago

Yes, that's the nature of client side loaded translations... On server side, the translations are ready, but on client side they need first to be loaded. So they are not yet ready on the first render loop. That's why in the example it is checked for the ready flag and if not ready a fallback will be rendered: https://github.com/i18next/i18next-http-backend/blob/master/example/next/pages/client-page.js#L11 btw: normally it is only a warning, not an error: "Warning: Expected server HTML to contain a matching text node for "loading translations..." in

."

image

fyi: I'm not a Next.js expert, but I think there is nothing we can do here if you want to use client side loading. Correct me if I'm wrong @isaachinman

isaachinman commented 2 years ago

Yes, what @adrai described is a correct explanation of what is happening. It's important to note that that is happening specifically because of this user's choice of backend.

In NextJs apps, the correct pattern is to:

  1. SSR or SSG โ€“ย all translations are available
  2. Serialise current locale and namespaces to pageProps
  3. Client render picks up translations from pageProps on first render, thus no hydration errors
Christian-Schoenlein commented 2 years ago

I am aware of the pattern but I was trying to translate the navbar & footer of my app using a Layout which cannot use SSR or SSG since it's not a NextPage.

I also tried using the ready flag which doesn't help because it doesn't solve the problem that whats rendered on the server isn't the same as what's rendered on the client initialy.

But I found a solution instead of having the translation in the return statement directly I moved it in a useEffect which only gets executed on the client side and stored the translation in a useState value. On the server the useEffect doesn't run and the translated string is undefined and on the client it will be undefined for the first render because the translation file is not available yet since its async.

Before with hydration error

image

After with useEffect & useState

image
isaachinman commented 2 years ago

which cannot use SSR or SSG since it's not a NextPage

That is simply not true. You just need to pass the appropriate namespace via your page itself, or put it as defaultNS.

Christian-Schoenlein commented 2 years ago

That is simply not true. You just need to pass the appropriate namespace via your page itself, or put it as defaultNS.

In Next.js Layouts are located inside the custom app component which is not a next page.

From the docs:

Inside your layout, you can fetch data on the client-side using useEffect or a library like SWR. Because this file is not a Page, you cannot use getStaticProps or getServerSideProps currently.

So you can't just pass down the namespace because the layout lives above every single next page and wraps around them.

isaachinman commented 2 years ago

I can assure you it's possible.

Zerebokep commented 2 years ago

A solution for this problem should be included in the readme file, as the example doesn't work (at least not without errors).

isBatak commented 2 years ago

With React 18 you can enable useSuspense option

/**
 * @type {import('i18next').InitOptions & Pick<import('next').NextConfig, 'i18n'>}
 */
module.exports = {
    backend: {
        backendOptions: [
            { expirationTime: 60 * 60 * 1000 },
            {
                /* loadPath: 'https:// somewhere else' */
            },
        ], // 1 hour
        backends: typeof window !== 'undefined' ? [LocalStorageBackend, HttpBackend] : [],
    },
    i18n: {
        defaultLocale: 'en-US',
        locales: ['en-US'],
    },
    serializeConfig: false,
    use: typeof window !== 'undefined' ? [ChainedBackend] : [],
    react: {
        useSuspense: true,
    },
};
thienna commented 2 years ago

any update?

bryanltobing commented 2 years ago

why this is closed with no solution? I think this is related to the rename of hydrate functionality in react 18 to hydrateRoot https://nextjs.org/docs/messages/react-hydration-error https://reactjs.org/docs/react-dom.html#hydrate

it works fine in react 17 though

bryanltobing commented 2 years ago

With React 18 you can enable useSuspense option

/**
 * @type {import('i18next').InitOptions & Pick<import('next').NextConfig, 'i18n'>}
 */
module.exports = {
  backend: {
      backendOptions: [
          { expirationTime: 60 * 60 * 1000 },
          {
              /* loadPath: 'https:// somewhere else' */
          },
      ], // 1 hour
      backends: typeof window !== 'undefined' ? [LocalStorageBackend, HttpBackend] : [],
  },
  i18n: {
      defaultLocale: 'en-US',
      locales: ['en-US'],
  },
  serializeConfig: false,
  use: typeof window !== 'undefined' ? [ChainedBackend] : [],
  react: {
      useSuspense: true,
  },
};

with useSuspense set to true the error disappear, but somehow it breaks some assets used in nextjs, like the use of next/image,

@isBatak

isBatak commented 2 years ago

@bryantobing12 how can that be related? Maybe next/image breaks when it's wrapped in <Suspense>. Could you check that in isolation?

bryanltobing commented 2 years ago

@isBatak I'm not sure. next/link SPA seems to break as well

bryanltobing commented 2 years ago

wrapping my top component with <Suspense> fix the issue

<React.Fragment>
  <React.Suspense fallback={<div>Loading</div>}>
    <NProgress stopDelayMs={0} />
    <Component {...pageProps} />
  </React.Suspense>
</React.Fragment>
ObaidQatan commented 7 months ago

I managed to solve this by preventing any client translations from being returned before the server-client cycle completes. Something like if (!hasRendered) return null, but it's kinda exhaustive to put this line before the return statement of every client component. Instead, I've adjusted my useTranslations hook a bit, making it return only the same values that were passed initially.

This's how useTranslations return statement looked like (Hydration Error):

return {
    translation: (key: string) => data?.[key] ?? key,
    loading: isLoading,
    locale: loc,
    dict: data ?? {}, // NOTE: This is useful for passing translations as a prop, and for debugging.
    refetchTranslations: refetch,
  };

Added Lines (to the hook itself, not components):

  const [hasRendered, setHasRendered] = React.useState(false);

   React.useEffect(() => {
    setHasRendered(true);
  }, []);
if (!hasRendered) {
    return {
      translation: (key: string) => key,
      loading: true,
      locale: loc,
      dict: {},
      refetchTranslations: refetch,
    };
  }

You may also return null if not yet rendered if you'd use the translations function accordingly in your code.