Git Product home page Git Product logo

Comments (19)

antfu avatar antfu commented on July 30, 2024 1

One drawback though, is that you can't direct access evaluating in the template then. Might be better to pass it as a ref (or evaluating & evaluatingRef as the same time, not sure)

from vueuse.

antfu avatar antfu commented on July 30, 2024

I want it several times too! Looks promising and let's have it!

Some thoughts,

  • for the naming, I think asyncComputed is simpler & neater.
  • for the returning value, I am thinking if we could get rid of the tuple, I would like it to be more like computed (sometimes ppl don't care if it's evaluating)

How about:

  const state = reactive({ value: defaultValue, evaluating: false }) as { value: T, evaluating: Boolean }
  /* ... */
  return state

from vueuse.

loilo avatar loilo commented on July 30, 2024

I agree on the naming thing, I just decided on use* to stick with the rules. 😁

However, I disagree on the usage of reactive, for various reasons:

  • It wouldn't actually bring it close enough to computed. While access in code would be equivalent in code usage (computedThing.value vs asyncComputedThing.value), it would differ in template usage (computedThing vs asyncComputedThing.value).

    More in general, I'd avoid naming a property of a reactive object value. This would probably be a real stumbling block for people not familiar with the code base, and they might assume asyncComputedThing is a ref because it has a .value.

    So, if we'd go the reactive() road, we should probably name it something like state or result.

  • However, I'd assume that people mostly really don't care for the evaluating part. Returning a tuple makes it easy to just ignore that part by omitting it from destructuring. Having an object with two properties, on the other hand, would mean additional work for all those people who don't care about evaluating because they had to use the extra accessor to get the computed value.

Alternative solutions:

  • Omit the evaluating part.

    This was how I implemented the function in the first place. However, I realized that there are probably enough use cases for it (e.g. showing a loading state instead of stale data) that having this would be desirable.

  • Provide the data in another way.

    We would not have to return the evaluating data. An alternative approach would be to provide another callback.

    So instead of using the function like this...

    const [downloads, isEvaluating] = asyncComputed(onCancel => {
      onCancel(() => { /* abort fetch */ })
    
      return fetch(/* and so on */)
    }, 0)

    ...we could provide something like a onStartStop (naming is debatable, don't like it quite yet) callback which is triggered when the computed starts/finishes evaluating:

    const isEvaluating = ref(false)
    const downloads = asyncComputed(({ onCancel, onStartStop }) => {
      onCancel(() => { /* abort fetch */ })
      onStartStop(state => {
        isEvaluating.value = state
      })
    
      return fetch(/* and so on */)
    }, 0)

    Advantage: It would keep the return value clean, which would supposedly remove noise and mental burden for most users.
    Disadvantage: It would slightly complicate the previously very simple callback argument (we'd have to provide a destructurable object à la { onCancel, onStartStop } instead of just pass the onCancel function) for a smaller fraction of users.

from vueuse.

antfu avatar antfu commented on July 30, 2024

Actually, you can pretend any object to be a ref like

const data = ref(defaultValue)
const evaluating = ref(false)

const state = {
  __v_isRef: true,  // this will make Vue think it's a ref
  get value() {
    return data.value 
  },
  get evaluating() {
    return evaluating.value
  }
} as Readonly<Ref<T> & { evaluating: Boolean }>

return state

This is also how toRef implemented.

So basically, asyncComputed will return a Ref but with an additional property evaluating. Just like the computed ref actually has an additional property effect if you don't know.

What do you think?

from vueuse.

loilo avatar loilo commented on July 30, 2024

This is fascinating.

Still one complaint though: evaluating now is no longer reactive. We could of course return the ref itself instead of its value (so it could be accessed as state.evaluating.value), but this modification of the ref type seems to be a little off the beaten track for Vue stuff.
Is there any known prior art using this technique? (The question is probably moot, given the relatively young age of the Composition API, but anyway.)

So, while I find this solution pretty cool from a DX point of view, I'd be careful to mess with known patterns. But I don't dislike it at all. Do you have any further place to gather feedback from regarding the shape of the API?

from vueuse.

antfu avatar antfu commented on July 30, 2024

evaluating now is no longer reactive

No, I believe it is. You can have a try.

Is there any known prior art using this technique?

AFAIK, no. I know some of my friends are using them in their private apps. And I am experimenting with a lot of things in @vue-reactivity (private too). I always want to use them in VueUse in order to get some clean interface but I haven't got time to do so. Let's be the first one!

I'd be careful to mess with known patterns

It's no harm as I can tell for now. I will be your eyes :P

from vueuse.

loilo avatar loilo commented on July 30, 2024

evaluating now is no longer reactive

No, I believe it is. You can have a try.

Sorry, I have not been clear enough with what I meant.

Of course, the state.evaluating getter would always return the correct value. But it would not be reactive in Vue's eyes (i.e. you could not use state.evaluating in a template and could not watch it).

This would be fixable though by implementing it like this:

const data = ref(defaultValue)
const evaluating = ref(false)

const state = {
  __v_isRef: true,  // this will make Vue think it's a ref
  get value() {
    return data.value 
  },
  evaluating
} as Readonly<Ref<T> & { evaluating: Boolean }>

return state

So we'd not return the current value for state.evaluating, but the ref itself, so it could be used in a template and accessed via state.evaluating.value.

from vueuse.

loilo avatar loilo commented on July 30, 2024

See comment above. 😁

from vueuse.

antfu avatar antfu commented on July 30, 2024

evaluating as Getter

const data = asyncComputed(async () => { /*...*/ })

return {
  data,
  evaluating: computed(() => data.evaluating)
}

evaluating as Ref

const data = asyncComputed(async () => { /*...*/ })

return {
  data,
  evaluating: data.evaluating
}

As an object of refs

const { data, evaluating } = asyncComputed(async () => { /*...*/ })

return {
  data,
  evaluating,
}

Or

return {
  ...asyncComputed(async () => { /*...*/ })
}

As an array of refs

const [data, evaluating] = asyncComputed(async () => { /*...*/ })

return {
  data,
  evaluating,
}

Yeah, I agree that the hack is not ideal to be used in templates. Maybe it's head back to the traditional implementation.

from vueuse.

loilo avatar loilo commented on July 30, 2024

Any concrete objections against the onStartStop approach from above? (As I said, naming is terrible, it's just as a proof of concept.)

from vueuse.

antfu avatar antfu commented on July 30, 2024

I don't think we actually need onStartStop as I can just put it at the end of my async function. About the onCancel part, I don't think we should provide a function call here. Instead, an object called cancelToken might be better:

something like:

asyncComputed((cancelToken)=>{
	const a = await fetch(xxx)

    if (cancelTolen.cancelled)
       return // early return
    
    cosnt b = await fetch(a)

    return [a, b]
})

from vueuse.

antfu avatar antfu commented on July 30, 2024

Inspiration from VS Code Extensions https://vscode.readthedocs.io/en/stable/extensions/patterns-and-principles/#cancellation-tokens

from vueuse.

loilo avatar loilo commented on July 30, 2024

I don't think we actually need onStartStop as I can just put it at the end of my async function.

Sorry, I don't get it. Could you elaborate on that? What could you put at the end of your async function?

Instead, an object called cancelToken might be better

I don't really care about the implementation details. (My inspiration for onCancel came from the Composition API's very own watchEffect.)
Regarding the cancel token: How would you implement a notification system on the token? EventEmitter is a Node-only API, EventTarget is web-only. Implementing a custom event emitter just for our little function would be overkill, so the remaining options would probably be a Vue instance with $on/$emit or a very reduced emitter (only allowing a single listener assigned to e.g. a cancelToken.onCancel property)?

from vueuse.

antfu avatar antfu commented on July 30, 2024

onStartStop

Let's keep the evaluating state inside the function, using events and let users doing that might be too much.

onCancel

I see. I agree it's better to stay with the Vue style. How about just keep it named as onInvalidate?


Sorry for being a bit nitpicking, this is a good direction and I'd say let's go for it. Thanks a lot 👍

from vueuse.

loilo avatar loilo commented on July 30, 2024

No worries, that's great. Let's be nitpicky now — we probably won't be able to change the design later. 🙂

Let's keep the evaluating state inside the function

Sorry, still don't get it — where would you like to keep it?

How about just keep it named as onInvalidate?

I deviated from the original name on purpose because the behavior is slightly different: watchEffect has no mechanisms to detect whether previous callbacks finished their work, therefore onInvalidate is always called when the next effect is invoked — whereas onCancel is only called when the next evaluation is started and the previous evaluation has not finished yet (which we can track thanks to the returned promise).

Of course we could resort to calling it onInvalidate, but then we'd also have to adopt the described watchEffect behavior to avoid confusion. However,

  1. we would somehow still need to tell the user whether the previous evaluation finished, likely with a parameter to the callback onInvalidate invokes:

    asyncComputed(onInvalidate => {
      onInvalidate(hasFinished => {
        // `hasFinished` would be a boolean indicating whether the
        // previous evaluation has finished, naming is up for debate.
      })
    
      return somePromise
    })

    However, this would then again deviate from the watchEffect behavior as their onInvalidate does not provide such a boolean.

  2. I think it's reasonable to not provide users with more information than they need.

    They should be notified when evaluation is canceled because they may have to do cleanup work, but I don't see any value in them being notified about the next evaluation if their own task has already finished.

Therefore I settled for onCancel.

from vueuse.

antfu avatar antfu commented on July 30, 2024

Let's keep the evaluating state inside the function

I think it's good to keep your original version :p

const [downloads, isEvaluating] = asyncComputed(onCancel => { })

Therefore I settled for onCancel

I got it, thanks a lot for the detailed explanation. Let's go for it!

from vueuse.

loilo avatar loilo commented on July 30, 2024

My pleasure. Want me to create a PR?

from vueuse.

antfu avatar antfu commented on July 30, 2024

It will be great if you don't mind. Thanks 😁

from vueuse.

loilo avatar loilo commented on July 30, 2024

I'll give it a try. First contact with Storybook for me, but the other functions should provide enough learning material. 🙂

from vueuse.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.