Vue: asyncComputed

Created on 1 Nov 2016  路  16Comments  路  Source: vuejs/vue

Have you tried vue-async-computed ? It is incredibly awesome, because it allows asynchronous computed properties. Among other things, this makes it incredibly easy to source your Vue data from AJAX calls. I will try to convince you that this feature is SO good, that it is worthy of including in Vue's core instead of being relegated to this lowly plugin.

Code sample

Here's an example application that uses two properties to seamlessly interact with the server. Actions are "dispatched" by setting properties. (I clump mine in a object called "active".) This flags them as dirty, causing Vue to automatically re-compute any async properties that depend on that property.

Here, $vm.active.lang is a read-write property containing the current language. The $vm.i18n property contains all the i18n strings. Changing $vm.active.lang causes Vue to pull the appropriate language file from the server and update the text on the page. Is that simple or what?

<template>
  <p>{{i18n.hello}}, {{CurrentUser.name}}!</p>
</template>

<script>
const Ajax = (url, opts) => window.fetch(url, opts).then((response) => response.json())

let App = new Vue({
  el: '#app',
  data () {
    return {
      active: {
        lang: 'en'
      }
    }
  },
  asyncComputed: {
    i18n () {
      return Ajax('/lang/' + this.active.lang)
    },
    CurrentUser () {
      return Ajax('/logged_in_user.json', {credentials: 'include'})
    }
  }
})
</script>

I also left in some code that fetches the current user from the server so we can greet the user by name.

How will it make current work-arounds straightforward?

Right now, it requires conceiving the idea of "async properties", searching the web to see if someone has done it, and then using this plugin. But I really think it is extremely elegant and intuitive, and having it baked into Vue's core and documented in the Vue guide would encourage more people to think, "Oh, this is a really great way to get data from my server into Vue" and would simplify a lot of people's spaghetti code, which would make the world a better place.

What potential bugs and edge cases does it help to avoid?

Many beginners (and even experts) still have not fully committed to using a Flux-architecture framework, or are dealing with horrendous legacy applications and "vueifying" pieces of it as we go. But it is well established that this unidirectional data flow from server to client is a great idea and simplifies state management and reduces spaghetti code. Adding native support for asynchronous properties would enable and promotes a very simple, declarative unidirectional server-to-client data flow that has the advantage of being easily injected into existing legacy code without having to learn a new framework.

I'm sure there are other uses for asynchronous computed properties too (dispatching a long-running task to a WebWorker comes to mind) but I've only used it for AJAX so that's the example I've used.

feature request intend to implement

Most helpful comment

I like the idea... maybe we can simply make normal computed accept returning a Promise.

All 16 comments

I like the idea... maybe we can simply make normal computed accept returning a Promise.

As long as that doesn't break existing code, I'm all for fewer keywords/names to remember.

Several quick thoughts on this:

  1. how can user declare a default value when promise is not resolved?
  2. should vue support a builtin throttle promise to handle events like keychange, mousemove?
  3. should vue support a builtin callback when certain promise is resolved?
  4. how to inspect whether the async property is resolving or resolved?

It might need a proposal for this.

@HerringtonDarkholme very good points. After thinking about it, I almost think all these issues you mentioned would be better solved with Rx...

With vue-rx the original example can be written as (simplified for the sake of discussion):

  new Vue({
    data: {
      lang: 'en'
    },
    subscriptions () {
      return {
        i18n: this.$watchAsObservable('lang')
          .switchMap(lang => Rx.Observable.fromPromise(fetch('/lang/' + lang)))
          .startWith({
             // default i18n settings...
          })
      }
    }
  })

The Rx version is obviously more verbose than the simple Promise version, but here we get almost everything @HerringtonDarkholme listed: default value, throttling and side-effect subscriptions (which in most cases eliminate the need for detecting when the Promise resolves).

If fetching is commonly used, with a little helper the above can be further simplified:

  // long name just to precisely describe what it does
  Vue.prototype.$reactivelyFetchAsObservable = function (urlOrGetter) {
    return this
      .$watchAsObservable(urlOrGetter)
      .switchMap(url => Rx.Observable.fromPromise(fetch(url))
  }

  new Vue({
    data: {
      lang: 'en'
    },
    subscriptions () {
      return {
        i18n: this.$reactivelyFetchAsObservable(() => {
          return 'lang/' + this.lang
        }).startWith({
          // ... default settings
        })
      }
    }
  })

To sum up, I think while the async computed pattern is simple and elegant for the basic use case, it may not be sufficient to deal with the full scale complexity of async operations. While we can shoehorn the async computed pattern to deal with these cases one by one, at that stage you'd probably be better off just going with Rx.

So unfortunately, I'm closing this. Note this is not saying that this is a bad idea - I think it's still a good idea to let the vue-async-computed library evolve on its own and maybe land on a good balance, but it's probably not the best idea to do that in core.

@yyx990803 to get the example above to work properly you have to add a .pluck('newValue') after the $watchAsObservable.

Vue.prototype.$reactivelyFetchAsObservable = function (urlOrGetter) {
  return this
    .$watchAsObservable(urlOrGetter)
    .pluck('newValue')
    .switchMap(url => Rx.Observable.fromPromise(fetch(url))
}

It's a very common case to have dependent properties, for instance:

Dropdown A.
Dropdown B which values depends on selected value of dropdown A
Input C which might be defaulted based on selected value of Dropdown B.
Now imagine that all these dependent values are retrieved from async calls from rest API.

I can't use computed anymore since they are all asynchronous. And yes, I can implement it with rxjs, but that's not as trivial as the computed way.

While I was learning vue, that was one of the first issues I was trying to solve and when I found this solution, I couldn't understand it. After a few days I came back to this very same issue and now with more knowledge of ReactiveX I can understand the solution.

OK, my point is:
Shouldn't we have a computed style way to solve this very common problem? It would be less verbose, more readable and it won't require additional knowledge.
Probably that's not for vue core library, but it might be in vue-rx, or even in this third party library using vue-rx behind the scenes. It could provide a really elegant way to solve this problem.

A quick search on GitHub. https://github.com/foxbenjaminfox/vue-async-computed

Use it at your own risk.

Sorry, probably I wasn't clear enough, but that library was the one that I first found and it made me wondering why such scenario wasn't handled in a similar way in vue core or any official vue library. I was expecting that at some point a similar solution would be in vue or any of its official libraries.

Please refer to https://github.com/vuejs/vue/issues/6004#issuecomment-312404196

We strive to keep API surface small.

why such scenario wasn't handled in a similar way in vue core or any official vue library

asyncComputed can be implemented in user land without hack. It isn't hard for users to use it in existing projects.

I would also argue that a half-baked asyncComputed wouldn't suffice users' need. Fully implementing it might introduces no simpler concepts nor smaller code size for async computed than for RX and its vue binding.


Now imagine that all these dependent values are retrieved from async calls from rest API.

Your use case might be done without async at all. Probably you can store value in another field and defining one computed property to extract value. Handling async is just setting the value in callback.

Drive-by impressions:

Coming over from Aurelia and Durandal I've found this to be bizarre. I expected it to just work.

Surely updating data from API responses queried based on user input is a common use-case for computed properties.

Would the cache of a computed not be desired in these types of uses? Why would you be forced to re-roll all these features from scratch when you have a structure which updates based on input and caches the results until they change again (exactly what you need for an async query)?

Your use case might be done without async at all. Probably you can store value in another field and defining one computed property to extract value.

Seen a few suggestions to use a hook once at startup to fill data. In contrast to my experience where most use-cases for these types of updating properties are the other way around. You're often querying small sets out of much larger data-sets which would be impractical to load at startup.

edit: I realise watch exists and probably suits these cases too. Find it's AngularJS style clunky though.

After reading this discussion I went for RXjs to try out the async computed subscriptions: https://github.com/cpietsch/harvest/blob/master/src/components/Dashboard.vue#L211
When handling 2 or more async properties you have to use mergeMap().

+1 for a native solution

  1. how can user declare a default value when promise is not resolved?
computed: {
  syncProperty () { return 'foo' },
  promisedProperty () { return Promise.resolve('foo') }, // default is undefined
  promisedPropertyWithDefault: {
    value () { return Promise.resolve('foo') },
    default: 'bar'
  },
  async asyncProperty () { return await Promise.resolve('foo') },
  asyncPropertyWithDefault: {
    async value () { return await Promise.resolve('foo') },
    default: 'bar'
  }
}
  1. should vue support a builtin throttle promise to handle events like keychange, mousemove?

even if a promise-based implementation of throttled events makes sense in particular scenarios, imho it's tangential to the main use case here. it would be sufficient to add a minimal core implementation that lets you asynchronously fetch data with the absolute minimum amount of boilerplate - then see what other clever and unexpected (but semantically correct) uses people put it to

  1. should vue support a builtin callback when certain promise is resolved?

can you provide a scenario where this is necessary - as opposed to normal vue-agnostic use of promises? watch seems a good way of doing this without expanding the api surface

  1. how to inspect whether the async property is resolving or resolved?

default value + callback on resolve is good enough for the core minimal use case

overall vue-async-computed is a step in the right direction, and core support for this would be a major step forward, seeing how promises and async/await are now a core part of our language workflow

In my @tozd/vue-observer-utils package I have $await helper with a very simple (basic) implementation:

const awaitResultsMap = new WeakMap();

function $await(promise) {
  if (!awaitResultsMap.has(options.key)) {
    const dep = new Vue.util.Dep();
    dep.depend();
    promise.then((result) => {
      awaitResultsMap.set(options.key, result);
      dep.notify();
    });
  }
  return awaitResultsMap.get(options.key);
};

It works pretty well to bridge Promises and reactivity in Vue.

@yyx990803 The package is mature enough now to merge into the core, yeah?

Also curious if your views have changed on this since your last review of the approach?

Same, being able to use async computed would be useful...
What about it?

Was this page helpful?
0 / 5 - 0 ratings