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

force re-computation of a computed property #214

Closed
jcaxmacher opened this issue Mar 31, 2014 · 40 comments
Closed

force re-computation of a computed property #214

jcaxmacher opened this issue Mar 31, 2014 · 40 comments

Comments

@jcaxmacher
Copy link

Is there a way to force re-computation of a computed property when none of the values the property depends on have changed?

I have a computed property that depends on some of the model data as well as the current time. So, the value of the computation changes over time. I'd like to force re-computation on an interval to capture the time dependency. Right now, I'm using a value on the root VM that is being toggled on an interval is a "dummy" dependency of the computed property. This works, but it seems like there should be a better solution.

@yyx990803
Copy link
Member

yeah, I can add something like vm.$recompute('key')

@yyx990803 yyx990803 added this to the 0.11 milestone Jun 20, 2014
@yyx990803
Copy link
Member

yyx990803 commented Oct 3, 2014

Rethinking the problem, it's probably better to do:

var vm = new Vue({
  data: {
    currentTime: Date.now()
  },
  computed: {
    /* ... */
  },
  created: function () {
    var self = this
    setInterval(function () {
      self.currentTime = Date.now()
    }, 1000)
  }
})

@pea3nut
Copy link

pea3nut commented Sep 13, 2017

I think vm.$recompute('key') is a good idea, if a computed property depends on DOM element attributes , I need to manually update the calculation of attributes.
otherwise, I have to turn all dependent DOM attribute into vue component data to change the computed property.

@benjarwar
Copy link

benjarwar commented Jan 3, 2018

I also have an edge use case for vm.$recompute, which involves needing to reference a DOM element that may be hidden depending on the breakpoint. If it's hidden, but the underlying data changes, my computed values end up being incorrect, and some features aren't rendered properly on window resize. If I had this method, I could simply recompute on window resize once the DOM element is available again.

@sirlancelot
Copy link

Your DOM shouldn't have any sources of truth. When using Vue, the DOM should simply be a view in to your data. If you have data that only exists in the DOM you're gonna have a bad time.

@pea3nut
Copy link

pea3nut commented Jan 4, 2018

@sirlancelot Why shouldn't I have any sources of truth from DOM? Vue is an incrementally-adoptable JavaScript framework. I'd like to use Vue as a jQuery-like tool, not entrusted all DOM to Vue.

I admit that Vue has a full-abstract power to let that I need not operate DOM, but sometimes, I just want to use Vue to do some simple action.

@benjarwar
Copy link

@sirlancelot it's a bit tough to explain, but that's not I'm doing. My data/computed values remain the source of truth.

To go into more detail, I'm referencing DOM elements to render an indicator element under a selected tab UI. The indicator transitions/animates in width and left position as a new tab is clicked. But, for small/mobile screens, a different UI is used (a select dropdown). Both the select and the tabs utilize the same data, let's call it selectedIndex.

To render the width/left position of the indicator, I have a computed method, let's call it getStyles, which returns the width and left offset of the button that corresponds with the selectedIndex value.

The problem arises if the user changes the selectedIndex via the select UI while on a small viewport. Since the value of selectedIndex changes while the tabs aren't displayed, I can't get the width/left offset for the selected tab. Therefore, getStyles doesn't return the correct value. And if the user then switches to the medium breakpoint, the tabs are re-displayed but the indicator isn't styled properly. Edge case, but no bueno.

My current solution is to move the tabs offscreen via CSS (instead of doing a v-if or display: none;) such that they can still be inspected by getStyles whenever selectedIndex changes. But that causes other headaches (such as the buttons being in the tab hierarchy). If I could manually $recompute on breakpoint change, it'd be a little saner and more explicit.

Sorry for the length. I do think the enhancement warrants further investigation and could be useful :)

@yeedle
Copy link

yeedle commented Mar 16, 2018

What if a computed property depends on a server call and I want to offer a refresh button? Could be a good use case for $recompute.

@nwaughachukwuma
Copy link

Like inline expressions, Vue.js automatically collects dependencies for computed properties and refreshes/recomputes them when their dependencies change. All you may need to do is reference those dependencies for which you need a recomputation. Check following for example: http://optimizely.github.io/vuejs.org/guide/computed.html

@yeldiRium
Copy link

yeldiRium commented Jun 27, 2018

I have a very similar use-case to @benjarwar.
I also want to position an element based on the offset of another element.

I currently do so by retrieving the element's boundingClientRect via this.$refs.elementName.getBoundingClientRect() inside a computed property.
Vue seems to not pick up on this and update on changes. Probably (not sure about what goes on behind the scenes) because each call returns a new BoundingClientRect object.

Is there a way around this, @nwaughachukwuma? Because if not, I'm also in favor of $recompute in some way or the other.

I think this.$recompute("key") would be very practical.

I realize that I could retrieve this data in an interval and update my component's data instead of using a computed property.
However, this seems more computationally expensive to me, since it would also re-retrieve the data when I don't explicitly need it. And since I use the component in question many times per site (potentially up to hundreds), I don't want to recollect more often than necessary.

EDIT:
In the meantime i recommend this very very dirty hack:

...
data() {
  return {
    forceRecomputeCounter: 0
    ...
  };
},
computed: {
  updateMePlox() {
    this.forceRecomputeCounter;
    ...
  }
  ...
},
methods: {
  thisShouldTriggerRecompute() {
    this.forceRecomputeCounter++;
    ...
  }
  ...
}
...

This is in my opinion relatively little boilerplate (although as I said very dirty) and let's you recompute on demand.

@benjarwar
Copy link

@yyx990803 can we get a reconsideration of this functionality? I think there are several use cases outlined above where $recompute would provide a less hacky way of getting values. Especially when dealing with responsiveness and needing to calculate layout styles across multiple DOM elements.

I get that $recompute could be ripe for abuse, or have unintended side effects. But the current workarounds feel more anti-patterny to me than providing an explicit method.

@psr1981
Copy link

psr1981 commented Jul 17, 2018

I have another use case 👍
I am trying to calculate inactive markers on leaflet based on the time elapsed. markers are generated as computed property.

now i need to refersh markers every 5 mins. i am not able to do that without wiring it up thru vuex store which seems wrong.

i should be able to ask vue to recompute markers every 5 mins... that should be the ideal solution here.

@qnp
Copy link

qnp commented Aug 8, 2018

Greetings,
I use a workaround to trigger a recompute : I set the computed property to depend on a dummy property such as const dummy = this.recompute. When I need a recompute to be triggered elsewhere, I just do something like: this.recompute = Math.random().

@sirlancelot
Copy link

Adding a recompute method is not very data-driven. As such, I don't expect the core team to implement this ever. Especially consider that Evan has already provided a far easier to understand solution as the second comment on this issue. It will make more sense once you understand Vue's data-driven dependency tree.

@posva
Copy link
Member

posva commented Aug 8, 2018

It can also be implemented in userland as a plugin, I played around with it and will publish a plugin soon just to show how easy it is to expand Vue features 😄

But, you should really revisit the way you are designing your data. I have never found myself in the need of such thing building Vue apps, and the points said by @sirlancelot are true

@sirlancelot
Copy link

Here's what I could come up with: https://codepen.io/sirlancelot/pen/JBeXeV It uses a Vue internal mechanism though...

@posva
Copy link
Member

posva commented Aug 8, 2018

that's pretty neat! This is what I did: http://jsfiddle.net/5y32ano7/2/ I added a separated recomputed option so it doesn't impact other computed properties

@awmottaz
Copy link

@posva thanks! I extended your idea to include computed properties which use getters and setters: http://jsfiddle.net/0wmsvp27/3/

@posva
Copy link
Member

posva commented Nov 21, 2018

BTW, I put that into small plugin: https://github.com/posva/vue-recomputed

@qnp
Copy link

qnp commented Nov 22, 2018

@posva thanks for this plugin (BTW I found a typo in README:

Never do this kind of thigs )

@posva
Copy link
Member

posva commented Nov 22, 2018

send a PR! 😄

@thedamon
Copy link

thedamon commented May 2, 2019

I ran into a case that had me really wishing to be able to do this:
I'm using dynamic Vuex modules, and in a computed property the vuex module has not been registered yet so it is never recomputing. since i'm namespacing the module based on a prop, i have to register the module inside the mounted hook.

computed: {
    currentTab() {
      return this.storeActive && this.$store.getters[this.namespace + '/currentTab']
    }
  },
  mounted() {
    if (!this.$store.state[this.namespace]) {
      this.$store.registerModule(this.namespace, store);
    }
    this.storeActive = true;
  },

I created an arbitrary data binding (storeActive), but it would have been awesome to just call this.$recompute('currentTab') instead

@sirlancelot
Copy link

but it would have been awesome to just call this.$recompute('currentTab') instead

@thedamon I created a $recompute() solution a few comments up but personally I think your storeActive is a better solution because it better describes your data model's dependencies. A $recompute() method is imperative-driven and sort of against Vue's design goal.

@space-dweeb
Copy link

We need $recompute()... Come on, please 👍 There is still no good solution for this almost 6 years later?

@sirlancelot
Copy link

We need $recompute()... Come on, please 👍 There is still no good solution for this almost 6 years later?

Literally the comment right above yours...

@loilo
Copy link

loilo commented Jan 6, 2020

Was in need of this recently. For the few cases where this is really needed (i.e. where you would introduce a data property with a counter or another random value just to invalidate stuff), I made a tiny AbortController-inspired package.

Check it out if you know what you are doing. 😉

@adamreisnz
Copy link

Still no support for $recompute...?

@thedamon
Copy link

thedamon commented Feb 2, 2020

Does this.$forceUpdate cause computed to recompute?

@loilo
Copy link

loilo commented Feb 2, 2020

@thedamon No, it just re-creates the DOM of the component based on its state, but it doesn't modify the state (including computed).

@arthabus
Copy link

arthabus commented Apr 7, 2020

was looking for the same thing but eventually disabling cache in computed property solved it in my case. Maybe it would be helpful for someone else here too:

Example:

computed: {
  example: {
    cache: false,
    get: function () {
      return Date.now() + this.msg
    }
  }
}

Found it here

@sirlancelot
Copy link

Note that disabling the cache is probably a really bad idea. You will basically wind up re-computing that value every time it is accessed which, depending on your discipline, could be multiple times per render. I believe the cache option is also missing from Vue v3 so you'd be actively writing yourself in to a box preventing you from using the latest version of Vue. Please be sure to read this entire thread and understand the implications before you use the cache option.

@alvaro-canepa
Copy link

Maybe this article has the solution.

@ryan-olejnik
Copy link

ryan-olejnik commented Jul 10, 2020

Unless your computed value accesses a reactive property (this article give a great explaination of reactive properties), then the computed method will not know to re-run.

You can manually force a computed method to re-run by calling:

      this._computedWatchers.myComputedValue.run();
      this.$forceUpdate();

Note that you must manually updated after, as the forced compute doesn't seem to cause a re-render.

@aliakhtar
Copy link

FYI for anyone else, just make it a method rather than computed. Much better for your stress levels.

@AimForNaN
Copy link

@aliakhtar Computed properties with cache disabled are equivalent to methods without arguments. Indeed, this is all about cached computed properties. If cache is disabled for the computed property, then it would always be computed when accessed.

@tmladek
Copy link

tmladek commented Jul 29, 2020

The problem with no $recompute is that while I understand "Vue's data-driven dependency tree", Vue exists in, and for, an environment which very much isn't data-driven, i.e. the web. It would be nice to not to have to interact with the DOM at all, but as of now, it's often an unfortunate necessity.

As many have stated before, all the current solutions feel more like anti-patterns than an actual $recompute method would be - from incrementing a meaningless counter, to shuffling around variables just so that they're copied verbatim from the DOM inside the component.

Maybe an annotation or an attribute that explicitly means "this getter has a dependency outside of the component, and should be recomputed on this event or time interval" would satisfy both camp $recompute and the purists?

@lzl124631x
Copy link

lzl124631x commented Jul 31, 2020

FWIW, my scenario is that I'm creating a wrapper class <Video> which conditionally show YouTube iframe or <video> tag.

My client code wants to get the currentTime as if <Video> is just <video> -- this.$refs.video.currentTime.

In my Video component initially I have

  get video() {
    return this.$store.state.youtubeVideoId
      ? this.$refs.youtubeVideo
      : this.$refs.localVideo;
  }
get currentTime() {
  return this.video.currentTime; 
}

It works for YouTube video but doesn't work for local video because video.currentTime is not reactive in vue for <video>.

I don't want to change this currentTime to a getCurrentTime method because it's a breaking change to the client code.

I ended up just creating a local reactive variable.

  private localVideoCurrentTime = 0;
  get currentTime() {
    if (this.video === this.$refs.localVideo) {
      // When it's local video, the currentTime is not reactive, so must read the reactive value using local variable.
      return this.localVideoCurrentTime;
    }
    return this.video.currentTime;
  }
onTimeUpdate() {
  this.localVideoCurrentTime = this.video.currentTime;
}

@heroselohim
Copy link

      this._computedWatchers.myComputedValue.run();
      this.$forceUpdate();

@ryan-olejnik post was the solution for me. So I added a wrapper helper to my main boot code:

Vue.prototype.$forceCompute= function(computedName, forceUpdate /* default: true */) {
	if (this._computedWatchers[computedName]) {
		this._computedWatchers[computedName].run();
		if (forceUpdate || typeof forceUpdate == 'undefined') this.$forceUpdate()
	}
}

Then you can call in your view or component

this.$forceCompute('title')

this.$forceCompute('title', false) // No force update

recompute as a name seems like a "patch" to a bug. So $forceCompute has more sense, following the same idea as $forceUpdate method.

@zanzara
Copy link

zanzara commented Jul 13, 2021

Unless your computed value accesses a reactive property (this article give a great explaination of reactive properties), then the computed method will not know to re-run.

You can manually force a computed method to re-run by calling:

      this._computedWatchers.myComputedValue.run();
      this.$forceUpdate();

Note that you must manually updated after, as the forced compute doesn't seem to cause a re-render.

Apparently this doesn't work anymore on Vue3. Can anybody confirm?
_computedWatchers is unkonwn

@rudolfbyker
Copy link

Here is my take on it, using composition API: https://stackoverflow.com/a/74403125/836995

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

No branches or pull requests