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

TypeError: this.$t is not a function in an async method, after component is not rendered anymore #990

Open
5 tasks done
CamilleDrapier opened this issue Apr 25, 2022 · 13 comments
Labels
has the workaround help wanted Extra attention is needed

Comments

@CamilleDrapier
Copy link

CamilleDrapier commented Apr 25, 2022

Reporting a bug?

I get the following error when I try to call $t, in an async method that makes the current component not being rendered anymore:

Uncaught (in promise) TypeError: this.$t is not a function
    toggle DisappearComponent.vue:13
    step tslib.es6.js:100
    verb tslib.es6.js:81
    fulfilled tslib.es6.js:71
    promise callback*step tslib.es6.js:73
    __awaiter tslib.es6.js:74
    __awaiter tslib.es6.js:70
    toggle cjs.js:11
    0 DisappearComponent.vue:8
    callWithErrorHandling runtime-core.esm-bundler.js:155
    callWithAsyncErrorHandling runtime-core.esm-bundler.js:164
    invoker runtime-dom.esm-bundler.js:369
    addEventListener runtime-dom.esm-bundler.js:319
    patchEvent runtime-dom.esm-bundler.js:337
    patchProp runtime-dom.esm-bundler.js:401
    mountElement runtime-core.esm-bundler.js:4632
    processElement runtime-core.esm-bundler.js:4595
    patch runtime-core.esm-bundler.js:4515
    componentUpdateFn runtime-core.esm-bundler.js:5066
    run reactivity.esm-bundler.js:185
    setupRenderEffect runtime-core.esm-bundler.js:5185
    mountComponent runtime-core.esm-bundler.js:4968
    processComponent runtime-core.esm-bundler.js:4926
    patch runtime-core.esm-bundler.js:4518
    mountChildren runtime-core.esm-bundler.js:4714
    mountElement runtime-core.esm-bundler.js:4623
    processElement runtime-core.esm-bundler.js:4595
    patch runtime-core.esm-bundler.js:4515
    mountChildren runtime-core.esm-bundler.js:4714
    processFragment runtime-core.esm-bundler.js:4885
    patch runtime-core.esm-bundler.js:4511
    componentUpdateFn runtime-core.esm-bundler.js:5066
    run reactivity.esm-bundler.js:185
    setupRenderEffect runtime-core.esm-bundler.js:5185
    mountComponent runtime-core.esm-bundler.js:4968
    processComponent runtime-core.esm-bundler.js:4926
    patch runtime-core.esm-bundler.js:4518
    render runtime-core.esm-bundler.js:5686
    mount runtime-core.esm-bundler.js:3903
    mount runtime-dom.esm-bundler.js:1593
    <anonymous> main.ts:16
    ts app.js:1069
    __webpack_require__ app.js:849
    fn app.js:151
    1 app.js:1082
    __webpack_require__ app.js:849
    checkDeferredModules app.js:46
    <anonymous> app.js:925
    <anonymous> app.js:928

More specifically, I think the problematic case can be broken down as follow:

  • Have a component with a method with an await call (slow) followed by calling $t
  • Call this method
  • Make the component not being rendered anymore before the aforementioned methods complete
  • Expect the $t call to happen and respond normally after the await part is finished

Expected behavior

I think I should be able to run a method without having to care about the fact that the component is not being rendered anymore after an await call.

Please let me know if this assumption is not reasonable, as I'm not completely sure it is fine. Also if you think something is to be fixed on the vue-core side and not in this repo, please do tell me as well 🙇

Reproduction

Please use the async-translation branch in this repository: https://github.com/CamilleDrapier/vue-i18n-test/tree/async-translation

Clicking on the "Hide and display translation" button triggers an error

System Info

System:
    OS: Linux 5.15 Manjaro Linux
    CPU: (12) x64 Intel(R) Core(TM) i7-9850H CPU @ 2.60GHz
    Memory: 16.48 GB / 30.99 GB
    Container: Yes
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 16.14.2 - ~/.nvm/versions/node/v16.14.2/bin/node
    Yarn: 1.22.18 - ~/.nvm/versions/node/v16.14.2/bin/yarn
    npm: 8.5.0 - ~/.nvm/versions/node/v16.14.2/bin/npm
  Browsers:
    Chromium: 100.0.4896.127
    Firefox: 99.0.1
  npmPackages:
    @vue/cli-plugin-eslint: ~4.5.0 => 4.5.15 
    @vue/cli-plugin-typescript: ~4.5.0 => 4.5.15 
    @vue/cli-service: ~4.5.0 => 4.5.15 
    @vue/compiler-sfc: ^3.0.0 => 3.2.33 
    @vue/eslint-config-standard: ^5.1.2 => 5.1.2 
    @vue/eslint-config-typescript: ^7.0.0 => 7.0.0 
    vue: ^3.2.33 => 3.2.33 
    vue-i18n: ^9.2.0-beta.35 => 9.2.0-beta.35

Screenshot

No response

Additional context

At least in my real-life application that was built with vue2 // vue-i18n v8, I was able to do similar things with no problems until I tried upgrading to vue3 // vue-i18n v9.

If this is still supported then maybe the problem is that the translation plugin of the component is unloaded/removed too early in the life-cycle?

Could be related to: #609

Validations

@CamilleDrapier CamilleDrapier added the Status: Review Needed Request for review comments label Apr 25, 2022
@kazupon
Copy link
Member

kazupon commented Apr 26, 2022

Thank you for your reporting!

I’ve checked this issue in your reproduction repo.
After await we get an error with the call to $t.

This issue is really weird ... 🤔
$t call before await works correctly, but after await, it seems that $t has been removed from the context of this (proxy).
we need to find out deeply further to see if this issue is caused by vue/core.

I have tried various workarounds, such as saving this to self but have not been able to find one.

@kazupon kazupon added help wanted Extra attention is needed and removed Status: Review Needed Request for review comments labels Apr 26, 2022
@CamilleDrapier
Copy link
Author

CamilleDrapier commented Apr 27, 2022

Thank you for looking into this! 💟

This could be a vue3 limitation, as I found this comment in the proposal breaking change: Now are internally on-vnode hooks with the exact same lifecycle as components. I am not completely sure I understand the implications (cf: https://v3-migration.vuejs.org/breaking-changes/custom-directives.html)

As for workarounds, one that could work is using the "global" i18n instance at that point... but having to do this (or even storing the this.t$ function in a local variable within the async method) would make our application quite difficult to maintain, as we would have to screen all uses of $t and change some of them for our migration (which is not that bad), but would also have to constantly keep in our mind that using translation can potentially break in an async method. Just for reference, I made an example on the async-translation-hack branch of my copy of the repository

@CamilleDrapier
Copy link
Author

Another workaround that seems to be working is the following, but I'm unsure if that is a reasonable enough workaround, and if it will not backfire horribly in some cases?

  app.use({
    install(appInner) {
      appInner.config.globalProperties.$t = i18n.global.t;
      appInner.config.globalProperties.$te = i18n.global.te;
    },
  });

I made a small branch with this change on my repo under async-translation-hack-install

Also, I do not know if that is related to this, but inside my real-life application, if I put my current component to the console when i18n's $t is having this "undefined" problem, it seems that other plugins such as $route or $store are available at that point.

@kazupon
Copy link
Member

kazupon commented May 12, 2022

Thank you for your workaround idea! ❤️

I seem that your ideas work fine if you use global scope in legacy API mode, APIs like $t and $te are available.
If your application uses both locale scope and global scope, it will not work correctly.

@CamilleDrapier
Copy link
Author

Thank you for the feedback, I'll be using this in my app for now (we only use global scope), and think about using the composition mode later after I manage to migrate to vue3 😄

@BARNZ
Copy link

BARNZ commented Jun 6, 2022

Getting a few of these errors coming through lately as well. Its as simple as having an ajax call in a page mounted hook and then calling this.$t after awaiting it. If the user happens to go back and forth quickly between pages the component can be mounted and unmounted in quick succession triggering the error. This problem is difficult to guard against from within my application code - on every usage of $t need to check if its first available in the component...

If I console.log(this) in the mount hook using options API:

Normal case - when everything works fine, $t, $tc etc are all available:
image

Failure case - $t and similar functions unavailable.
image

@edwinofsakh
Copy link

It looks like in legacy mode we use mixin that removes $t on unmount hook.
https://github.com/intlify/vue-i18n-next/blob/v9.2.2/packages/vue-i18n-core/src/mixins/next.ts

@DomasB
Copy link

DomasB commented Oct 27, 2022

Hi,
We are facing the same issue in our app. I can see that the change mentioned by @edwinofsakh was added a while back, but the goal is not clear from PR:
https://github.com/intlify/vue-i18n-next/pull/36/commits

Is there any plan to fix this?

@frknbasaran
Copy link

frknbasaran commented Nov 5, 2022

I'm facing with this issue too. I created another workaround to by-pass this issue like below.

data() {
   return {
      msg: {
         key: this.$t('localizationKey')
      }
   };
},
methods: {
  async someMethod() {
     await someAsyncProcess();
     
     alert(this.msg.key);
  }
}

@etischenko
Copy link

etischenko commented Aug 13, 2023

The issue is still relevant. Are there any plans to fix this? We are in the process of migrating the app to Vue3, and I guess we'll have to look for such cases throughout the whole app manually, which is not much fun :(

this.confirmDialog(
        this.$t('common.dialogTitleConfirm'),
        this.$t('sessions.sessionCard.confirmDelete'),
        async () => {
          this.notifyWarning(this.$t('sessions.sessionCard.someKey')); // works
          await this.asyncMethod(this.session);
          this.notifySuccess(this.$t('sessions.sessionCard.someKey')); // TypeError: this.$t is not a function
        },
      );

UPD: seems that CamilleDrapier's workaround helped; we'll stick to it for now. But I hope it will be fixed at some point.

@lk77
Copy link

lk77 commented Oct 4, 2023

Hello,

i have this exact same issue and i found a fix :

let plugin = {
    install: (app) => {
        let i18nApp = Object.assign({}, app, {
            mixin: (m) => {
                // We don't want unmounted hook because we want i18n to still work after component unmounting
                delete m.unmounted;

                app.mixin(m);
            }
        });

        i18n.install(i18nApp);
    }
}

we simply remove unmounted hook from the i18n mixin, so $t is still available after component destruction

i think removing $t and other global properties lead to unexpected behaviours down the line and is a bad idea.

In my particular use case,
I have a form in a prompt, the prompt close upon submit, and the component is unmounted, but the notification is triggered when the response come back a few seconds later, and i display a notification, and i need $t for that

@mahnunchik
Copy link

I've faced with exactly the same issue. Could it be fixed for the async methods without hacks?

@BobbieGoede
Copy link
Member

This isn't necessarily related to async methods, there shouldn't be any issues using i18n inside of an async method as long as you're not unmounting the current component before accessing component methods, I updated the reproduction to demonstrate this in a stackblitz project here. This edge case is probably not present when using the composition API.

I think you may run into other issues if you rely on the component instance after unmount, as described here certain functionality such as reactive effect will be stopped after unmount, so you may run into unpredictable behavior (not just related to vue-i18n).

There are a few ways to work around the issue in the reproduction:

  • Store the translation result in a variable before the unmount is triggered and use it instead of calling the i18n method
  • Use this.$root.$t instead, though this probably uses the global i18n scope (see commented code in reproduction)
  • Prevent unmounting the component by using v-visible instead of v-if
  • Use composition API, this is recommended over mixins as described here

This issue is not straightforward to 'fix', I guess instead of deleting the keys on the component they could be replaced with those used by the global/root scope? Maybe this would lead to other confusing behavior but it would prevent erroring.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
has the workaround help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

10 participants