DEV Community

Julien for Monisnap

Posted on

💡 Vue Typescript State Management : We can do better than “isLoading” in 2022

Sorry for the clickbait title, but I needed your attention 👀 

Have you ever encountered code like this one :

new Vue({
  el: '#app',
  data () {
    return {
      info: null,
      loading: true,
      errored: false
    }
  },
  filters: {
    currencydecimal (value) {
      return value.toFixed(2)
    }
  },
  mounted () {
    axios
      .get('https://api.coindesk.com/v1/bpi/currentprice.json')
      .then(response => {
        this.info = response.data.bpi
      })
      .catch(error => {
        console.log(error)
        this.errored = true
      })
      .finally(() => this.loading = false)
  }
})

...

<div id="app">
  <h1>Bitcoin Price Index</h1>

  <section v-if="errored">
    <p>We're sorry, we're not able to retrieve this information at the moment, please try back later</p>
  </section>

  <section v-else>
    <div v-if="loading">Loading...</div>

    <div
      v-else
      v-for="currency in info"
      class="currency"
    >
      {{ currency.description }}:
      <span class="lighten">
        <span v-html="currency.symbol"></span>{{ currency.rate_float | currencydecimal }}
      </span>
    </div>

  </section>
</div>
Enter fullscreen mode Exit fullscreen mode

It’s an example I found in the Vue documentation here https://vuejs.org/v2/cookbook/using-axios-to-consume-apis.html#Dealing-with-Errors

What if you have multiple things that could load, do you add a loading2 variable ? 👀

To solve this issue, you can use a variable for each async actions you have with this 4 “states” :

  • IDLE : the user didn’t trigger the action yet
  • WAITING : the action is ongoing
  • ERROR : there was an error
  • DONE : The action succeeded

Using an enum with the different states, and better naming, the code can be rewritten like this :

enum AsyncState {
  IDLE = 'IDLE',
  WAITING = 'WAITING',
  ERROR = 'ERROR',
  DONE = 'DONE',
}

new Vue({
  el: '#app',
  data () {
    return {
      info: null,

      AsyncState,
      currentPriceLoadState: AsyncState.WAITING,
    }
  },
  filters: {
    currencydecimal (value) {
      return value.toFixed(2)
    }
  },
  mounted () {
    axios
      .get('https://api.coindesk.com/v1/bpi/currentprice.json')
      .then(response => {
        this.info = response.data.bpi
        this.currentPriceLoadState = AsyncState.DONE
      })
      .catch(error => {
        console.log(error)
        this.currentPriceLoadState = AsyncState.ERROR
      })
  }
})

...

<div id="app">
  <h1>Bitcoin Price Index</h1>

  <div v-if="currentPriceLoadState === AsyncState.WAITING">Loading...</div>
  <section v-else-if="currentPriceLoadState === AsyncState.ERROR">
    <p>We're sorry, we're not able to retrieve this information at the moment, please try back later</p>
  </section>

  <section v-else>
    <div v-for="currency in info" class="currency">
      {{ currency.description }}:
      <span class="lighten">
        <span v-html="currency.symbol"></span>{{ currency.rate_float | currencydecimal }}
      </span>
    </div>

  </section>
</div>
Enter fullscreen mode Exit fullscreen mode

With better naming and this simple enum, you can almost cover all the use cases where you need to load one or multiple things and manage errors ✨

If you want to manage different error messages, you can add another variable with an enum type like this :

enum CurrentPriceLoadErrors {
  INVALID_CURRENCY = 'INVALID_CURRENCY',
  API_LIMIT_REACHED = 'API_LIMIT_REACHED',
  DEFAULT = 'DEFAULT',
}
Enter fullscreen mode Exit fullscreen mode

Tell me in the comment section if you like this trick or not, and if you have an even better technique !

And don’t forget to like and share this post if you liked it 💙

Discussion (8)

Collapse
jcuervas profile image
jcuervas

Although is an elegant solution, you are just replacing a boolean loading for a state loading, as each element need a different variable to track it's loading state in both cases.

Collapse
monisnapjulien profile image
Julien Author

Thanks a lot for the comment ! :)

Indeed it's not a time saver, you don't use less code. However, the proposed way is, in my opinion, for easier readability, extensibility and consistency by using a single variable with a better naming to handle a common "async action state", including the error state that is sometime forgotten 😅

Collapse
justintime4tea profile image
Justin Gross • Edited on

As an alternative one could also store the promise of each async chain (axios->then->catch, asyncSomething -> then, ...) and then use Promise.all to wait for them all to finish. Just remember Promises are eagerly evaluated in JS so they immediately begin executing upon their creation.

const asyncThingOne = ...
const asyncThingTwo = ...
Promise.all([asyncThingOne, asyncThingTwo, ...]).then(updateIsLoadingToFalse)

(The above is a mix between pseudo code and code, the elipse is like in literature not the spread op)

Better still, I like to apply the "waiting on data" pattern to those elements which are waiting on data and those elements which are not waiting on data I immediately render. For example if you have a list of items that are populated with data, you render out all the layout and page and where there are "cards" or "items" that are constructed from fetched data you add a pulsing grey "loading element" which switches to a populated thing once the data is fetched. Imagine YouTube search where the thumbnails have yet to be loaded. It's a popular pattern. With pagination you could load 3-5 items at a time and the elements in the UI would "load in" over time. With server side events or web sockets you could "stream" the data to the page and avoid polling all together.

Collapse
monisnapjulien profile image
Julien Author • Edited on

Thank you for the comment 🙏

I also really like this approach, in Flutter I saw something similar with Riverpod AsyncValue which I found very intuitive and readable 😉

Collapse
justintime4tea profile image
Justin Gross • Edited on

That's a great example! I work in Flutter and Dart as well and like the approach from Riverpod.

Other examples, albeit on the more extreme end of the gradient, are most things in Rust. There is no null ; you have Option of Some generic over T or None, and Result with either an Ok generic over T or Err (Data baring enums). In rust they go so far as to make it a compiler error to not address the result of an async op or handle all "control flow" operation "cases". You can "cheat" with handling funcs which return Result and "ignore" the result by saying "let _ = await someOpWhichReturnsOptionOrResult" and you can add a "default case" to a "switch like op" but you must consciously make those decisions; it's not by happenstance or by ignorance. The compiler also gives you the answer 99% of the time (exaggerating but it's often the case the compiler tells you exactly what to do); The errors in Rust are the most helpful messages I've ever been graced with by a conpiler. I am in love with Rust if you can't tell lol.

Collapse
vadim profile image
Vadim Vinogradov

Still isLoading, but more fancy. I would love something more declarative, but have no idea how. Thanks for sharing!

Collapse
monisnapjulien profile image
Julien Author

Thank you for the comment !

You can maybe create a component that takes a promise in parameter and 3 slots :

  • loading component
  • error component
  • success component using the result of the promise (passed in props maybe ?)

In Flutter there is Riverpod AsyncValue, but in Vue it's not as easy and type-safe. Hope that helps 😉

Collapse
vadim profile image
Vadim Vinogradov

Interesting! I will try. Thank you