2

tldr How can I set a class variable from within an Promise

I'm banging my head as I'm too stupid to use async/awaitwith javascript. There are of course plenty of examples and blog posts, but they are 'then'inning the results only into console.log, what I don't need.

My use case is very simple, I want to load translations from a json using fetch (if they haven't been loaded yet) and then return the translated value with a function translate.

I thought if I use then, then the execution is paused until the Promiseresolves or fails.

class Test {

    constructor() {
    }

    /**
     * Loads translations
     */
    async _loadTranslations() {
        console.log("Loading tanslations");
        let response = await fetch('data.json');
        let json =  await response.json();
        return json;
    };

    translate(key, language) {
      if(!this.translation){
         this._loadTranslations().then(data =>{
            console.log("data is loaded!", data);
            this.translation = data;});
       }
       return this.translations[language][key];
    }

}

console.log("translation",new Test().translate("MEGA_MENU_CMD","de-DE"))

But it always logs translation undefined.

I don't want something like

  new Test()._loadTranslations().then(r => console.log("result",r))

as I want to use the translate function within templates and I don't want to use .then in all of my templates.

Edit I don't see how I can use the fetch to get data from an API and set it as the model of a class. The thens and callbacks in my opinion do different things. Or how is the correct way to instantiate a class, load data from an API and then work with this data?

2nd edit:

In fact, I just want to create a TranslationMixin and load the translations once. Then in my page (I'm playing around with Polymer) I want to use return html '<div>${this.translate("de-De","propertyX"}<div>' and that's why I don't want a Promise but just the plain string. In fact the translation should be loaded during construction and done. But as fetch returns a Promise I'm stuck with the Promise and fail to get the value out (#sigh). So probably I stick just to regular XMLHttpRequest...

Frank D
  • 23
  • 4
  • 2
    Since `translate` relies on `_loadTranslations`, which is **asynchronous**, it **must** be asyncronous as well, hence you **must** either use `async/await` or follow the promise approach. Something like this should give you the hint you're looking for: https://pastebin.com/kGj5fPvZ – briosheje Sep 10 '18 at 14:29
  • Possible duplicate of [Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference](https://stackoverflow.com/questions/23667086/why-is-my-variable-unaltered-after-i-modify-it-inside-of-a-function-asynchron) – zero298 Sep 10 '18 at 14:33
  • Yes, I know. But I want to use the `translate` function in some template and there I want to see the value and not the Promise. But I'll check the links in zero298 comment. – Frank D Sep 10 '18 at 14:44
  • @FrankD what do you mean by "In some template"? Are you using an external framework or something? There is another possible approach to solve the issue, which is registering an event (something like a "translationsLoaded" event), that allows you to avoid using promises, since once the event will be fire the translations **will be loaded**. – briosheje Sep 10 '18 at 14:48
  • I want to create a `TranslationMixin` and in my page I want to use `return html '
    ${this.translate("de-De","propertyX"}
    '` and that's why I don't want a `Promise` but just the plain string. In fact the translation should be loaded during construction and done. But as `fetch`returns a `Promise`I'm stuck with the Promise and fail to get the value out (#sigh).
    – Frank D Sep 10 '18 at 18:18

4 Answers4

2

You need to wait until the translation is finished. The easiest way to do that is to make translate an async function as well. This will return a promise and you can get the translation in the then():

class Test {

    constructor() {
    }

    /**
     * Loads translations
     */
    async _loadTranslations() {
        console.log("Loading tanslations");
        this.translation = "some translation"
        return this.translation;
    };

    async translate(key, language) {
      if(!this.translation){
         return this._loadTranslations().then(data =>{
            console.log("data is loaded!", data);
            return  data;});
       }
       return this.translation;
    }

}
new Test().translate("MEGA_MENU_CMD","de-DE")
.then(translate => console.log(translate))
Mark
  • 90,562
  • 7
  • 108
  • 148
  • Thanks, but I don't want a *Promise` but just the plain string. – Frank D Sep 10 '18 at 18:01
  • @FrankD that's not going to be possible because `translate()` is potentially asynchronous. You need some mechanism to know *when* that string is available. Promises (or callbacks) are that mechanism. – Mark Sep 10 '18 at 18:03
  • If you would make `translate` an `async` method, you would use `await` not `then`. – Bergi Sep 11 '18 at 08:54
0

If you want that you must put your translate Asynchronous aswell. So you need to use async and await on that function too.

Because he will actually do what your expect in the _loadTranslations() and he will await for a result, but since the function that is calling it is not async he will do everything till the end! I hope this helps.

Tiago Neiva
  • 337
  • 1
  • 6
0

This is the async/await solution you were looking for in the first place.

translate(key, language){
    return this.translation
    ? this.translation[language][key]
    : this._loadTranslations()
}

And since, you need to await the console.log too, and since await can only be inside an async function, you can do the following ..

(async()=>{
    console.log("translation", await new Test().translate("MEGA_MENU_CMD","de-DE"))
})()

Explanation

Function translate returns the result of this._loadTranslations() which is Promise. Then, the console.log statement awaits that Promise and outputs once it is fulfilled, that is, once you have the JSON from data.json.

rmn
  • 1,119
  • 7
  • 18
  • Thanks, as mentioned, I don't want to print it to the console. This works fine I know. I just need the plain string value from the translation, no Promise. I want to use the translated value in some other class and there I don't want to use a promise but the plain string. – Frank D Sep 10 '18 at 18:00
  • @FrankD Got it. But see this is how it is. Javascript is single threaded. But we want it to go fetch a file from the server - data.json. So, while it goes and does that, the browser can't just wait for it. Thats why all these even driven mechanism exists. Think of a it like a button click, when the button click happens, an event handler function gets called - the browser can't just halt all the processing and wait for the user to click the button. Similary in this case, you get a `Promise`, a `.then()` or a `async/await`. – rmn Sep 10 '18 at 20:25
  • And `async await` is the closest concept you'll get to reaching a code which works asynchronously, but *feels like* synchronous. So when you write the statement `await new Test().translate("MEGA_MENU_CMD","de-DE")`, it simply returns a string, like you want. – rmn Sep 10 '18 at 20:27
0

how is the correct way to instantiate a class, load data from an API and then work with this data?

Load data before instantiating the class, then the instance can use it synchronously.

You should load the translations in a static method before creating your instance, so that you won't have to wait for them in your translate method:

class Test {
    constructor(t) {
        this.translations = t;
    }

    /**
     * Loads translations
     */
    static async fromLoadedTranslations() {
        console.log("Loading tanslations");
        let response = await fetch('data.json');
        let json = await response.json();
        console.log("data is loaded!", data);
        return new this(json);
    }

    translate(key, language) {
        return this.translations[language][key];
    }
}

Test.fromLoadedTranslations().then(test => {
    console.log("translation", test.translate("MEGA_MENU_CMD","de-DE"));
    // or pass `test` to your polymer template
});

(async function() {
    const test = await Test.fromLoadedTranslations();
    console.log("translation", test.translate("MEGA_MENU_CMD","de-DE"));
    // or pass `test` to your polymer template
}());

There's no way to load data asynchronously and not wait for it (using either then or await) somewhere.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Ok, looks interesting. But also a bit tedious? I mean it's a regular use case to load data from an API and then work with it. I cant imagine that this is so difficult with Javascript ;) I mean I can do it the manual way with regular `XMLHttpRequest`, but isn't that a bit cumbersome? I know how to do it that way, but I thought to use the neat `fetch`-API would declutter my code. – Frank D Sep 10 '18 at 22:13
  • @FrankD I don't see what is tedious about this. It is much less cumbersome when the instance does not need to handle any asynchrony. (And no, you don't want to use synchronous requests in JS). – Bergi Sep 11 '18 at 08:51