Subscribe on changes!

Support asynchronous computed

avatar
Jun 3rd 2022

What problem does this feature solve?

This would allow

  • computation to rely on WebAPIs, for example IndexedDB
  • implementation of event loop non-blocking computation
  • implementation of debouncing / throttling or other custom use cases

What does the proposed API look like?

a = computed(async () => {
  return await IndexedDB.get(id.value) + 5;
});

...

await a.value // get value

You could say this feature works out of the box, but unfortunately dependency on b is not tracked in this example

a = computed(async () => {
  return await IndexedDB.get(id.value) + b.value;
});
avatar
Jun 4th 2022

A complete example would be helpful in understanding what your actual usecase is.

What's b? Where would you await a computed? And are you aware that collection of reactive effects is inherently synchronous in all kinds of effects in Vue?

avatar
Jun 4th 2022

In such a situation, the watch could come in handy

avatar
Jun 4th 2022
avatar
Jun 5th 2022
a = computed(async () => {
  let b = b.value // read anything that need tracking before the await
  return await IndexedDB.get(id.value) + b;
});
avatar
Jun 5th 2022

@sqal @lidlanca both of your proposed solutions have the same problem - I quote from https://vueuse.org/core/computedasync/

Dependencies that are accessed asynchronously will not trigger re-evaluation of the async computed value.

@lidlanca your proposed workaround does not cover some scenarios, for example

const getDependency = (key) => ...

const a = computed(async () => {
  const c = await IndexedDB.get(id.value);
  return getDependency(c).value;
});
avatar
Jun 5th 2022

@boussadjra thanks, unfortunately watch would not solve the problem example I provided above

avatar
Jun 5th 2022

@LinusBorg I try to keep examples laconic, so we do not dig into irrelevant details.

What's b?

Let's say b = ref(5). It could be any Vue computed / ref.

Where would you await a computed?

Anywhere where I want to use it, let's say before making request to some API.

And are you aware that collection of reactive effects is inherently synchronous in all kinds of effects in Vue?

Yes, I am aware. I don't see how it helps solving the problem.

avatar
Jun 5th 2022

irrelevant details.

I'm nto asking to get into irrelevant details, I'm asking to understand the use case and the kind of usage you have in mind.

for that, abstract one-line examples are not really useful.

Anywhere where I want to use it, let's say before making request to some API.

Can you write a short example how what would look like?

Yes, I am aware. I don't see how it helps solving the problem.

I wasn't saying it's solving the problem in any way. Quite to the contrary, I was pointing out that dependency collection in Vue is synchronous in general, not just computed, and as such is fundamentally unsuited, in its current form, for what I so far think you seem to want. This would be a pretty fundamental change.

avatar
Jun 5th 2022

@LinusBorg as I understand that's the point of having issues here in GitHub - discussing valuable changes (fundamental ones included). Please correct me if I'm wrong.

I provided few use cases in the very beginning:

  • computation to rely on WebAPIs, for example IndexedDB
  • implementation of event loop non-blocking computation
  • implementation of debouncing / throttling or other custom use cases

In my opinion your questions / requests are irrelevant and exhausting, please don't be surprised if I stopped replying you.

avatar
Jun 5th 2022

I'm not sure how I gave the impression that I want to stop discussion. I just pointed out the scope. If I gave that impression, sorry for that.

Also feel free to stop replying. Just consider that we likely won't implement something that we don't see a need for ourselves "and* for which we don't have concrete use cases / expectations / scenarios. That's what I wanted to get more understanding on with my questions - out of curiosity. But you are free to not help me understand better.

Me failing to fully imagine the desired usage from your short descriptions of course doesn't mean that all of our team will have the same trouble that I have, so maybe someone will understand and implement it.

Good luck!

avatar
Jun 5th 2022

@LinusBorg with all due respect I guess that your teammates of higher seniority (for example @yyx990803) would understand the problem well. Vue is amazing, thank you for contributing to it.

avatar
Jun 5th 2022

Lol

avatar
Jun 6th 2022

const getDependency = (key) => ...

const a = computed(async () => { const c = await IndexedDB.get(id.value); return getDependency(c).value; });

this does not handle all possible use cases, but it address a way for the use case you presented.


const getDependency = (key) => ...

// using vueuse to simplify handling of a computed based on a promise.
const a = computedAsync(async () => {
  return await IndexedDB.get(id.value);
});

const b = computed(()=>{
  if(a.value){
    return getDependency(a.value).value
  }
})
avatar
Jun 6th 2022

@lidlanca I agree, this could be a workaround in many cases, thanks!

However an ideal solution in my opinion would be native support for async computed in @vue/reactivity. I have nice use cases for this library in Node.js environment where VueUse does not belong I assume.

avatar
Jun 8th 2022

Dependencies that are accessed asynchronously will not trigger re-evaluation of the async computed value.

This is a fundamental limitation of how Vue's reactivity system works, unless JavaScript provides a mechanism to capture function execution context across async ticks.

What you can do is to ensure the deps you are interested in are accessed synchronously before entering the first await:

import { computedAsync } from '@vueuse/core'

a = computedAsync(async () => {
  const idVal = id.value
  const bVal = b.value
  return await IndexedDB.get(idVal) + bVal;
});

A form of async computed that tracks deps across async ticks could potentially be done by using a generator function, but either way there is no plan to provide this feature in Vue core because:

  1. This is a niche case with a great amount of complexity.
  2. Vue provides enough primitives to do this in userland.

Side note: @donatas-luciunas your interaction with @LinusBorg has been patronizing and borderline rude. I'm sure you wouldn't be pleased if I imply that your request roots from a lack a good understanding of how Vue's reactivity tracking works. Please be respectful when interacting with maintainers who are trying to help you. If you continue with this attitude, don't be surprised for not getting any further replies from the team.

avatar
Jun 8th 2022

@yyx990803

This is a fundamental limitation of how Vue's reactivity system works, unless JavaScript provides a mechanism to capture function execution context across async ticks.

I agree that it is not possible with the API I described, but you could change the API

const getDependency = (key) => ...

const a = computed(async (value) => {
  const c = await IndexedDB.get(value(id));
  return value(getDependency(c));
});

where parameter value is a unique function for every caller. Do you agree this could work?

What you can do is to ensure the deps you are interested in are accessed synchronously before entering the first await:

As I mentioned this workaround does not cover some scenarios, for example the one I provided above here.

This is a niche case with a great amount of complexity.

I do not agree on niche adjective. If you are interested let me share a bit of a vision. Let's assume @vue/reactivity supports async computed.

For example we have 3 separate components that rely on the same peace of data that is retrieved with fetch. We could use async computed for such scenario and minimal amount of fetch invocations would be ensured (invoked with first call; cached until invalidated; recomputed if watched).

Imagine not one peace of data, but whole business model expressed as computed values some depending on others. You could use dependency injection container (could be inversify for example) to avoid clutter. Again @vue/reactivity would ensure minimal amount of fetch invocations, which is more or less a target for every developer.

It seems that Vue tries to address this problem with async setup, but it has it's limitations. I wanted to offer this alternative approach, as it would ensure optimal runtime elegantly.

Is there a possibility you will reconsider supporting async computed?


I'm sure you wouldn't be pleased if I imply that your request roots from a lack a good understanding of how Vue's reactivity tracking works.

I believe we have a misunderstanding on what a "feature request" means. Do you mean that feature requests that are not easy to implement are not welcome?

If you continue with this attitude, don't be surprised for not getting any further replies from the team.

Why do you look at the speck of sawdust in your brother’s eye and pay no attention to the plank in your own eye? That's retorical.

avatar
Jun 8th 2022

...where parameter value is a unique function for every caller. Do you agree this could work?

No, because async dependency collection creates race conditions where each re-run of the async effect could produce different set of dependencies to be tracked. This is much more complicated than it may seem.

As I mentioned this workaround does not cover some scenarios, for example the one I provided above here.

You example is not technically valid, because the return value of IndexedDB.get can never be a reactive dependency. Vue's reactivity system implies that reactive dependencies are created before the effects runs - they cannot be created on the fly during an effect (and then tracked by the effect itself).

Is there a possibility you will reconsider supporting async computed?

No. I still believe it's a niche case and most common scenarios can be solved by synchronously accessing necessary dependencies upfront before starting going into async land.

I believe we have a misunderstanding on what a "feature request" means. Do you mean that feature requests that are not easy to implement are not welcome?

As maintainers, we not only have to gauge the cost of implementing something, but also the cost of maintaining it forever afterwards. To ensure it is worth prioritizing over other things that we have to spend time on (which is limited), we also need to consider how many users would actually benefit from it. In this case, we have decided to not honor this request due to the reasons I have already outlined.

Coming back to your question: "feature request" means it is entirely up to the maintainer to decide whether to honor it or not. You can disagree with our reasoning behind the decision, but that doesn't make us in any way obliged to implement it, or even continue to engage in the conversation.

Why do you look at the speck of sawdust in your brother’s eye and pay no attention to the plank in your own eye? That's retorical.

I have looked at the comments from both sides and I do not see anything inappropriate from @LinusBorg, and certainly nothing that warrants the patronizing tone you have used against him.

avatar
Jun 8th 2022

Dear Evan,

Let's begin with the fact that this issue is not about me, it is about improvement idea.

The recurring problem I notice is that you and @LinusBorg put too little effort to grasp the idea (good indicators are your mistakes for example "return value of IndexedDB.get can never be a reactive dependency") and as a result you reject the idea without proper reason.

My role here is contributor / guide as I understand. I am willing to trade off sounding patronizing instead of killing the idea without proper reason. If you are looking for someone to blame, I'd argue that it is you and @LinusBorg for making me choose. A side action - you could think how to improve the way you guys work so great ideas are not rejected without proper reason. Of course you are not obligated to implement them, the correct way to handle is to keep issue hanging in my opinion.

Now let's discuss why am I convinced you do not have proper reason. Let's assume you should close the issue if value is too little and cost is too high.

This is much more complicated than it may seem.

This part is about cost.

I agree that it is not simple. If it was, I would have provided a pull request :)

I still believe it's a niche

This part is about value.

You compare your reactivity model to spreadsheet. Spreadsheet supports async.

Tracking only first call dependencies sounds like a bug. I'm not sure if niche is the right word here. You cannot prove it is not needed feature by the fact that people do not use this feature if the feature does not work.

Let's assume any data can be async data. Such assumption is not niche as that's the reason async / await were introduced. Then encountering your dependencies tracking limitation is as niche as having if in computed. In other words - not niche at all.

Do you agree? Or can you prove otherwise?

avatar
Jun 8th 2022

Please note maintainers have no obligation to spend more effort than they deem necessary. I have responded with as much patience as I can, explaining why we are not interested in implementing it. In short: not worth it in core, maybe doable in userland for those who really need it. Again, you can disagree with the reasoning, but you are in no position to demand anything. I have no interest in engaging further on this topic.