React-i18next: React.lazy usage with useTranslation hook

Created on 4 Apr 2019  路  37Comments  路  Source: i18next/react-i18next

Describe the bug
I have a few pages that use React.lazy. It seems that on those pages, my translations aren't loaded (or maybe are loaded after the component renders).

I've tried several variations of setting react: { useSuspense: false} and useTranslation('translations', {useSuspense: false})` but have had no success getting translations to display.

I'm not sure if this is a bug, or a misconfiguration on my part.

Occurs in react-i18next version
10.6.1

To Reproduce

  1. Use the following config:
    i18n.ts
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-xhr-backend';
import {initReactI18next} from 'react-i18next';

i18n
  .use(Backend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    defaultNS: 'translation',
    fallbackLng: 'en',
    interpolation: {
      escapeValue: false, // not needed for react as it escapes by default
    },
    load: 'languageOnly',
    saveMissing: true,
  });

export default i18n;

Optionally add react: { useSuspense: false}

  1. Lazy load a component that uses the useTranslation hook:
const NotFound = React.lazy(() => import('./NotFound'));
import React from 'react';
import {useTranslation} from 'react-i18next';

const NotFound: React.SFC = () => {
  const [t] = useTranslation();
  return <h1 data-testid='not-found-heading'>{t('not-found')}</h1>;
};

export default NotFound;

Optionally use useTranslation('translations', {useSuspense: false})`

Expected behaviour
I expect to see my translations, not the key of the translation.

OS (please complete the following information):

  • Device: 2016 MBP 13" macOS 10.14.4
  • Browser: Firefox Developer Edition 67.0b6

All 37 comments

Could you provide a code sandbox for reproduction? Not yet sure - never used React.lazy yet - so not sure what it does under the hood...but as it's react official i guess it should work with hooks out of the box...

Sure! Let me get one together for you. 馃槃

thank you

Arg. I can't reproduce it in a CodeSandbox because the issue happens at a very specific point in time.

I've setup a router with a lazy loaded route. I've also setup a context provider for my authenticated user details. When a user is authenticated and refreshes the page, all components that are lazily-loaded do not have translations.

It seems to be a very edge case.Perhaps when full Suspense drops, this will not longer be an issue. Until then, I'll stop using React.lazy 馃槃

Sorry to bother!

Hm, strange...never had tested react.lazy...just assumed everything will work. Hopefully this get sorted out sooner or later - will also try to find some time to test and see what i can do - currently hard on time before weekend.

Actually, looks like it's still happening even when not using React.lazy. @jamuhl would you take a reproduction repo?

Okay, I figured out the problem! It seems I was doing this:

const [t] = useTranslation() and it was working 99% of the time.

When I updated all to use const [t] = useTranslation('translation') everything started working just across all edge cases.

It seems the resolve to figure out what namespace was taking too long and therefore loading the translations wasn't working?

I鈥檒l put together a repo reproduction. It seems to not happen in codesandbox, but does in CRA.

I have also encountered issues with using this with React.lazy, for components that are wrapped <Suspense> My set up is the same as @DevanB's. However, I am doing const { t } = useTranslation(). When navigating to the page, it wouldn't load and give the following error:

The page I'm trying to load is the LazyLogInPage

Warning: lazy: Expected the result of a dynamic import() call. Instead received: [object Module]

Your code should look like: 
  const MyComponent = lazy(() => import('./MyComponent'))

The above error occurred in one of your React components:
    in Unknown (created by LazyLogInPage)
    in Suspense (created by LazyLogInPage)
    in LazyLogInPage (created by Context.Consumer)
    in Route (created by Pages)
    in Switch (created by Pages)
    in Suspense (created by Pages)
    in Pages (created by Query)
    in div (created by ScrollablePage)
    in ScrollablePage (created by Context.Consumer)
    in Route (created by withRouter(ScrollablePage))
    in withRouter(ScrollablePage) (created by Query)

@YuhanLee please provide a codesandbox for reproduction

@jamuhl the issue with "please provide a codesandbox for reproduction" is that the issue doesn't seem to be reproducible in CodeSandbox.

Now that someone else has validated the issue, can I share my actual project code with you in lieu of a CodeSandbox?

@DevanB don't have to be codesandbox...but minimal...remove everything not needed....I do not have the time to look at any actual large project

@DevanB @YuhanLee personally I use react-loadable to split modules and got no issues so far

@DevanB don't have to be codesandbox...but minimal...remove everything not needed....I do not have the time to look at any actual large project

Sounds good. I'll try to get that together by the weekend.

@DevanB @YuhanLee personally I use react-loadable to split modules and got no issues so far

The issue with react-loadable (to me) is that it isn't the official React solution; React.lazy() is.

I'll get back to you as soon as I can produce the issue.

@jamuhl @DevanB I'm having the same issue where some components are seemly randomly unable to call t('...') to get back data (( t('test') will return 'test' as if the key doesn't exist )), while other components are able to. I did try switching my router from React Lazy to react-loadable, but the issue is still present. I'll try to post something with more context once I can better isolate the problem in my large app.

I don't think the problem is to do with React.lazy since replacing it with react-loadable had no effect for me... there's something else going on I think. I did notice that sometimes changing to a different page in the router and then switching back to problematic page causes it to work. Perhaps it's something to do with loading order..

Never mind, it was a user error. I was using a sub page "/test/foo" and my i18n localization file was set to a relative directory (so it was looking for /test/en.json instead of /en.json). Setting the location to being with "/" fixed the issue of the translation file not loading in.
TL;DR my issue was a user error

I'm experiencing problems with the hooks API as well. In our application, namespaces are dynamically added depending on user roles. Our problem is the other way round though: It only works for lazily loaded components. Normal components are not re-rendered with an updated t() function once new namespaces have been fetched. Therefore, they keep displaying default values instead of real translations.

Could it be that there's a problem with re-rendering when new namespaces come in and that the inconsistent results related to lazy as described in this issue are more of a sympton? Some sort of a race condition? Maybe some namespaces finish loading before the lazy-loaded components so that they are available in time for the initial mount whereas normally imported components are rendered right away and would require a re-render when new namespaces come in?

I've tried coming up with a minimal reproduction but also failed to get the exact same behavior outside of our application. However, it behaves a bit weird in a different way: Namespaces that are dynamically added to useTranslation() apparently aren't fetched from the backend at all. See https://codesandbox.io/embed/gifted-golick-5gtiq.

In my quest to narrow down the problem, I've noticed a few things. Just leaving them here, maybe it rings a bell for someone:

  • A while ago I've filed this PR to allow for dynamically updating namespaces after initial mount. The react-i18next code base has changed quite a bit since then. Has this feature been considered for the new hooks API?
  • I've tried different variations of bindI18nStore and bindI18n, but no luck so far. Would that be of any help here? This comment sounds kinda promising.
  • Manually loading namespaces with loadNamespaces() when they change at least fixes the loading issue in the CodeSandbox project linked above (there's some commented out code in there to try it out). But shouldn't i18next take care of that? It also doesn't help with that other problem.
  • <Translation key={ns} /> fixes the lazy problem in our own application. Kinda unsatisfying though to remount large parts of the entire application every time a namespace changes. I've actually used that same fix a few months ago before #520 was released.

I'd appreciate any ideas that might help debugging this!

@nicolasschabram not sure if that is related...changing the namespace in useTranslation was something I never thought users will do...

The loading happens only once in a useEffect because of https://github.com/i18next/react-i18next/blob/master/src/useTranslation.js#L77 -> you might change the empty array to [ns] like:

     // ...
     if (bindI18n && i18n) bindI18n.split(' ').forEach(e => i18n.off(e, boundReset));
      if (bindI18nStore && i18n)
        bindI18nStore.split(' ').forEach(e => i18n.store.off(e, boundReset));
    };
  }, [ns]); // define props to trigger using effect on rerender (none here)

Could you please check if that solves your problem?

Hi @jamuhl, thanks for your input. Seems like it works! I've prepared a PR: #878. Let me know if there's anything else I can help with to make this happen!

Hi!

I have a problem using useTranslation and Web Components. Here is the CodeSandbox.

In the TabComp component, if I change the line const { t } = useTranslation("", { useSuspense: false }); for const { t } = useTranslation(); an error occurs. The error occurs because the shadowRoot still haven't loaded its child elements and it's trying to access a child element.

I'm not sure if this is related to the react-i18next or React Suspense/Lazy.

Could someone help me?

Sorry, but I'm not used to webcomponents...I also do not get an error using chrome -> the Tab shows in blue like expected...no error in console....

Hi @jamuhl.
In order to get an error you should change the useSuspense from false to true.

@CristianDDias try

  React.useEffect(() => {
    if (
      !ref.current ||
      !ref.current.shadowRoot ||
      !ref.current.shadowRoot.firstElementChild
    )
      return;

    const content = ref.current.shadowRoot.firstElementChild.getElementsByClassName(
      "ui5-tc__content"
    )[0];
    content.style.background = "blue";
    content.style.padding = "8px";
  });

@jamuhl I tried this, but it didn't work. It should change the background and padding of the tab, but for some reason, when using useSuspense, the shadowRoot doesn't has its elements loaded.

I also created an issue in the React project, because maybe it is related to the React Suspense/Lazy.

@CristianDDias did that work for you?

@jamuhl, actually it didn't work :/

because even using the ready flag, when the useEffect is called, the ready is already true and the shadowRoot still doesn't have its elements loaded.

It needs to change the background color to blue in order to work correctly.

@CristianDDias Guess for now in this case you will have to work with useSuspense false for those components you need to access the shadowRoot.

Closing this for now...feel free to reopen if still an issue

During some of my browsing, I got to this issue. I am using React.Lazy as well and experienced similar behavior to what's described here (That is if I understood correctly).
What's happening for me is that some of my translations are missing from the lazy-loaded component, even when I see the network request for all those translations and even when I see them in the i18n instance when debugging.

In our app, we are using multiple namespaces, and the lazy-loaded component is using a different namespace.
What solved the issue for me was to define a namespace in the "root" of the lazy-loaded component.

This behaviour is "caused" because of this. Basically, because the component is lazy-loaded, React starts to render the other components when it thinks they are ready. Since this component is loaded at a later stage, the ns defined for this lazy-loaded component is the defaultNS - which is already loaded of course. The lazy-loaded component is marked as tReady and is rendered.

At a later stage, i18next detects that it needs to load some additional namespaces (Based on usage within other components) and loads them to the store. However, the components themselves aren't re-rendered since nothing really changed in them (All props, state etc. are the same) so a "missingKey" is displayed.

This describes the process (In concept)

  1. Root component tries to load with Suspense.
  2. react-i18next detects that we need to use suspense and holds off rendering until the defaultNS (Or some ns) loads from backend
  3. React renders the root component and begins loading our lazy-loaded component (The one wrapped with React.lazy). Suspense is triggered once again
  4. Component js returns from backend and rendering begins.
  5. useTranslation() triggers, but tReady flag automatically becomes true since the defaultNS (Or some other ns) is already loaded from step 2.
  6. Lazy-loaded component is rendered as usual - with "Missing keys")
  7. Additional namespaces are loaded from the backend and added to i18n store.

I actually think this behaviour (Of lazy-loaded) components with react-i18next should be documented somehow.

because of this...idk...not really...that is only a take default if nothing else is needed...like said...I never could reproduce this....and we have some lazy-loaded stuff in our production app.

A case for proper reproduction would help...

@jamuhl So here's a quick reproduction that I've created https://codesandbox.io/s/react-i18next-lazy-loading-example-zve2k
It shows what i tried to explain with words (Might have failed :) )
Check the "LazyLoad" file. When not defining a ns for the useTranslation hook, the defaultNS is used, but suspense is not triggered because the defaultNS is already loaded beforehand.

I don't think that's a bug or something.. Understanding why this behavior happens is enough in my opinion. Maybe something about this should be written somewhere? (Or not, you know best whether people had this situation)

@slavab89 but your sample is totally wrong...you're using namespaces inside a component but do not define that inside useTranslation -> how should it load that at all...using something like: t("lazy:lazyContent") does not magically load the namespace or assert it is loaded...if you're using namespaces define them in useTranslation...that's it.

@jamuhl I tried to modify @slavab89's codesandbox example as follow

-  const { t } = useTranslation();
+  const { t } = useTranslation("lazy");

But the example still fails to load the lazy-loaded translation:

Also, @slavab89 had commented that line, which seems to show that he tried it, so maybe I'm just not understanding your answer.

I'm myself failing to get useTranslation working with lazy components wrapped in suspense. Would you mind sharing an example since you say doing that in your production app?

My code looks like this:

const Admin: React.FC = () => {
  const { t } = useTranslation("admin");  // <-- Calling namespace in useTranslation, this should trigger loadNamespace !

  return (
    <div>
        {t("admin:dashboard")}
    </div>
  );
};

```tsx
const Loadable = (importStatement) => {
const Component = lazy(importStatement); // <-- The component is lazy !

return (props: Record) => (
...

}> // <-- The lazy component is wrapped in Suspense !!


);
};

```tsx
import Loadable from "./components/Loadable";

const AdminPage = Loadable(() => import("./pages/Admin")); 

export default function App(): JSX.Element {
  return (
    <div>
        <ErrorBoundary fallback={<h2>failed</h2>}>
          <Suspense fallback={<h2>loading</h2>}>
              <Route path="/admin/">
                <AdminPage />
              </Route>
          </Suspense>
        </ErrorBoundary>
    </div>
  );
}

This throws the following error

Note that if disabling suspense, I don't have any problem. But that's a shame when having react concurrent mode enabled...

const Admin: React.FC = () => {
  const { t, ready } = useTranslation("admin", { useSuspense: false });

  return ready ? (
    <div>
        {t("admin:dashboard")}
    </div>
  ) : null;
};

Idk...just made the change again: https://codesandbox.io/s/react-i18next-lazy-loading-example-forked-kv2cr works on my browser...

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Unexpected format output
ok2ju picture ok2ju  路  3Comments

Failing tests in react_withHOC
dawsbot picture dawsbot  路  4Comments

withTranslation HOC looks for translation in first namespace only
ezze picture ezze  路  4Comments

languageOnly not working, bug or misconfig?
a-barbieri picture a-barbieri  路  4Comments

Trans component support for react-native
tankpower1 picture tankpower1  路  3Comments