Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose onInvalidate on computed. #163

Open
ycmjason opened this issue Apr 25, 2020 · 6 comments
Open

Expose onInvalidate on computed. #163

ycmjason opened this issue Apr 25, 2020 · 6 comments

Comments

@ycmjason
Copy link

I bumped in to a situation where I would need to invalidate some computed value. I could do this currently using watch

const x = computed(() => ...)
watch(() => x.value, (_xValue, xOldValue, onInvalidate) => {
  onInvalidate(() => {
    invalidate(xValue)
  })
})

But ideally it would be nice if this invalidation logic is encapsulated in the computed.

My specific situation is that I am trying to create an object url as a computed value.

const objectUrl = computed(() => {
  return URL.createObjectURL(blob.value)
})

watch(() => objectUrl.value, (objectUrl, _, onInvalidate) => {
  onInvalidate(() => {
    URL.revokeObjectURL(objectUrl)
  })
})

It would be quite useful if computed expose onInvalidate so I can write:

const objectUrl = computed(onInvalidate => {
  const url = URL.createObjectURL(blob.value)
  onInvalidate(() => {
    URL.revokeObjectURL(url)
  })
  return url
})
@jods4
Copy link

jods4 commented Apr 25, 2020

I am not sure if it's the right design.

To me, as a primitive building block, computed evokes pure functions: caching a computation based on dependencies.
Side-effects make me think of watch.
This use-case is valid but falls into side-effects category IMHO, which would imply watch is the right building block for onInvalidate.

This use-case is clearly a value that needs cleanup, which is a rare thing in JS.
Maybe build something reusable on top of the computed concept?
Here's a readonly ref that always disposes the value it holds:

function disposableComputed(getter, dispose) {
  let value;
  return customRef((track, trigger) => {
    watchEffect(onInvalidate => {
      onInvalidate(() => dispose(value))
      value = getter()
      trigger()
    })
    return { get() { track(); return value } }
  })
}

Use it like this:

function blobToUrl(blob) {
  return disposableComputed(
    () => URL.createObjectURL(unref(blob)),
    url => URL.revokeObjectURL(url),
  )
)

@ycmjason
Copy link
Author

True! It can be done in user land and is pretty simple to do so. I guess you can do with just ref + watch + readonly.

const invalidableComputed = (getter) => {
  const r = ref()
  watchEffect(
    onInvalidate => {
      r.value = getter(onInvalidate)
    },
    { flush: 'pre' }, // not sure if this is a good use of flush lol never really need it
  )
  return readonly(r)
}

This use-case is valid but falls into side-effects category IMHO, which would imply watch is the right building block for onInvalidate.

Ya. I agree with you that computed is prettier and more ideal when it is pure. But it will miss out quite a few real-life use cases and forcing user to create pattern like the above.

Adding onInvalidate is not the only way to solve this, just trying to file this issue to let the maintainers aware of this use case. 😄

@jods4
Copy link

jods4 commented Apr 26, 2020

Note that wrapping ref with readonly is not proper usage. readonly is meant for reactive objects, whereas ref represents a reactive value.
Edited: this is wrong, the readonly proxy lets _isRef go through so it behaves as if it still was a ref.

Ya. I agree with you that computed is prettier and more ideal when it is pure. But it will miss out quite a few real-life use cases and forcing user to create pattern like the above.

Yes. Beyond "ideal" design, you're still free to have side-effects in a computed. What makes your use-case special is that you need to catch when the effect (computed or watch) stops for the final cleanup. That's the one special thing onInvalidate does for you (otherwise you can just call your cleanup code at the beginning of the computed).
In JS where (almost) all resources are managed for you, that's rather rare, IMHO.

BTW, onStop is exposed, although a bit hidden. You can also do this:

function blobToUrl(blob) {
  let url;
  const result = computed(() => {
    URL.revokeObjectURL(url);
    return url = URL.createObjectURL(url);
  })
  result.effect.onStop = () => URL.revokeObjectURL(url);
  return result;
}

That's an alternative way of writing the disposableComputed I proposed previously.

@ycmjason
Copy link
Author

Note that wrapping ref with readonly is not proper usage. readonly is meant for reactive objects, whereas ref represents a reactive value.
Of course, since a ref is a JS object it kind of "works" but it won't be considered a ref anymore, implying: you won't get auto-unwrap in template, it will work differently (badly?) when reassigned a new value in another object (scrap that, it's readonly!), unref won't work on it, isRef will return false, etc.

ref with readonly is not a proper usage? 😮

There is even a test for this:
https://github.com/vuejs/vue-next/blob/master/packages/reactivity/__tests__/readonly.spec.ts#L343L350

I am not so sure about if what you are saying about readonly is totally correct.

onStop is exposed, although a bit hidden. You can also do this:

Hmmm. Can't find anything regarding this in the doc. It is probably not intended for public use. I'll leave this issue open for now.

@jods4
Copy link

jods4 commented Apr 26, 2020

I am not so sure about if what you are saying about readonly is totally correct.

Thanks for pointing this out to me! You are right, it works 100%.
It really surprised me given there's nothing in code to handle that but after thinking about it I realized the internal field _isRef goes through the proxy, so isRef and co. still see the proxy as a ref.
I'm gonna edit my previous answer to not confuse anyone else reading!

Hmmm. Can't find anything regarding this in the doc. It is probably not intended for public use.

I can't say, maybe someone from Vue team can clarify. For some apis it's a bit unclear if they are public or not, given that the docs are not final.

I think it might be public because, as of beta-4:

  • effect is exposed on computed on purpose: to provide a mean to stop the computed. This has to be public API otherwise you can't stop a computed which could lead to memory leaks (outside a component where it's stopped automatically when unmounted).
  • The API of effect itself is public for people who use @vue/reactivity directly (there's no watch in this API, effect is the equivalent).

I'll leave this issue open for now.

Sure do! I'm just pointing out some ideas, not trying to shut you down 😄.
Maybe Evan will add onInvalidate to computed, or will add a kind of disposableComputed to core.

@ycmjason
Copy link
Author

ycmjason commented Aug 6, 2020

@yyx990803 just wanna ping you in case you missed this. 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants