Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React.lazy usage with useTranslation hook #817

Closed
DevanB opened this issue Apr 4, 2019 · 37 comments
Closed

React.lazy usage with useTranslation hook #817

DevanB opened this issue Apr 4, 2019 · 37 comments

Comments

@DevanB
Copy link

DevanB commented Apr 4, 2019

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
@jamuhl
Copy link
Member

jamuhl commented Apr 4, 2019

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

@DevanB
Copy link
Author

DevanB commented Apr 4, 2019

Sure! Let me get one together for you. 😄

@jamuhl
Copy link
Member

jamuhl commented Apr 4, 2019

thank you

@DevanB
Copy link
Author

DevanB commented Apr 4, 2019

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!

@DevanB DevanB closed this as completed Apr 4, 2019
@jamuhl
Copy link
Member

jamuhl commented Apr 5, 2019

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.

@DevanB
Copy link
Author

DevanB commented Apr 5, 2019

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

@DevanB DevanB reopened this Apr 5, 2019
@DevanB
Copy link
Author

DevanB commented Apr 5, 2019

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?

@jamuhl
Copy link
Member

jamuhl commented Apr 5, 2019

@DevanB
Copy link
Author

DevanB commented Apr 5, 2019

I’ll put together a repo reproduction. It seems to not happen in codesandbox, but does in CRA.

@YuhanLee
Copy link

YuhanLee commented Jun 5, 2019

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)

@jamuhl
Copy link
Member

jamuhl commented Jun 5, 2019

@YuhanLee please provide a codesandbox for reproduction

@DevanB
Copy link
Author

DevanB commented Jun 5, 2019

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

@jamuhl
Copy link
Member

jamuhl commented Jun 5, 2019

@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

@jamuhl
Copy link
Member

jamuhl commented Jun 5, 2019

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

@DevanB
Copy link
Author

DevanB commented Jun 5, 2019

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

@YuhanLee
Copy link

YuhanLee commented Jun 6, 2019

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

@jadbox
Copy link

jadbox commented Jun 12, 2019

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

@jadbox
Copy link

jadbox commented Jun 12, 2019

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

@nicolaslohrer
Copy link

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 Dynamically update ns prop on I18n component #520 was released.

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

@jamuhl
Copy link
Member

jamuhl commented Jun 19, 2019

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

@nicolaslohrer
Copy link

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!

@CristianDDias
Copy link

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?

@jamuhl
Copy link
Member

jamuhl commented Nov 1, 2019

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

@CristianDDias
Copy link

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

@jamuhl
Copy link
Member

jamuhl commented Nov 1, 2019

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

@CristianDDias
Copy link

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

@jamuhl
Copy link
Member

jamuhl commented Nov 1, 2019

@jamuhl
Copy link
Member

jamuhl commented Nov 1, 2019

@CristianDDias did that work for you?

@CristianDDias
Copy link

CristianDDias commented Nov 1, 2019

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

@jamuhl
Copy link
Member

jamuhl commented Nov 2, 2019

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

@jamuhl
Copy link
Member

jamuhl commented Jan 16, 2020

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

@jamuhl jamuhl closed this as completed Jan 16, 2020
@slavab89
Copy link

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.

@jamuhl
Copy link
Member

jamuhl commented Jun 27, 2020

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

@slavab89
Copy link

slavab89 commented Jun 27, 2020

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

@jamuhl
Copy link
Member

jamuhl commented Jun 27, 2020

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

@AdrienLemaire
Copy link

AdrienLemaire commented Sep 8, 2020

@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>
  );
};
const Loadable = (importStatement) => {
  const Component = lazy(importStatement);  // <-- The component is lazy !

  return (props: Record<string, unknown>) => (
    <Suspense fallback={<div>...</div>}>    // <-- The lazy component is wrapped in Suspense !!
      <Component {...props} />
    </Suspense>
  );
};
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;
};

@jamuhl
Copy link
Member

jamuhl commented Sep 8, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants