11

Here is an example of a Vuex Store with a parameterized getter which I need to map onto the Vue instance to use within the template.

const store = new Vuex.Store({
  state: {
    lower: 5,
    higher: 10,
    unrelated: 3
  },
  getters: {
    inRange: state => value => {
      console.log('inRange run')
      return (state.lower <= value) && (state.higher >= value)
    }
  },
  mutations: {
    reduceLower: state => state.lower--,
    incrementUnrelated: state => state.unrelated++
  }
})

new Vue({
  el: '#app',
  template: "<div>{{ inRange(4) }}, {{ unrelated }}</div>",
  store,
  computed: Object.assign(
    Vuex.mapGetters(['inRange']),
    Vuex.mapState(['unrelated'])
  ),
})

setTimeout(function() {
  console.log('reduceLower')
  store.commit('reduceLower')
  setTimeout(function() {
    console.log('incrementUnrelated')
    store.commit('incrementUnrelated')
  }, 3000);  
}, 3000);
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex/dist/vuex.js"></script>
<div id="app"></div>

Firstly, this does appear to be valid, working code. However considering computed is meant to be a cached set of computed properties, I'm curious about the behavior in this scenario, is there caching going on? If there isn't, is there a performance concern to consider? Even though the function does not cause any state change, should it be a method?

Is this an anti-pattern? The example isn't a real one, but I do want to centralize logic in the store.

UPDATE

I have updated the example to illustrate that modifications to the underlying lower/higher value upon which the inRange getter is based, are indeed reactive for the Vue instance (despite not being mapped as state). I have also included an unrelated value which is not part of the calculation, if the mapped getter were cached, modifying the unrelated value should not trigger the getter to be called again, however it does.

My conclusion is that there is no caching, thus this has poorer performance than a conventional computed property, however it is still functionally correct.

The question remains open as to whether there is any flaw in this pattern, or one available which performs better.

mikeapr4
  • 2,830
  • 16
  • 24
  • I also need to cache my getters with arguments. I'm still not sure how to do it. – mesqueeb Jun 17 '17 at 17:09
  • I've elaborated a bit on Matt's answer (had to add another answer for it): https://stackoverflow.com/a/44607740/1084004 – mikeapr4 Jun 17 '17 at 18:02

3 Answers3

5

In my opinion this is an anti-pattern. It's a strange way to funnel a method. Also, no, there isn't caching here since inRange immediately return a value (the final function) without using any members in state - so Vue detects 0 reactive dependencies.

Getters can't be parameterized in this way, they can only derive things that are based in state. So if the range could be stored in state, that would work (and would be cached).

Similar question here: vuexjs getter with argument

Since you want to centralize this behavior - I think you should just do this in a separate module, perhaps as a mixin. This won't be cached, either, so you would have to wrap it (and the input) in a component's computed or use some other memoization

Something like this:

import { inRange } from './state/methods';
import { mapGetters }  from 'vuex';

const Example = Vue.extend({
  data: {
    rangeInput: 10
  },
  computed: {
    ...mapGetters(['lower', 'higher']),
    inRange() {
      return inRange(this.rangeInput, this.lower, this.higher);
    }
  }
});
Matt
  • 43,482
  • 6
  • 101
  • 102
  • Thanks for the input, do you have an example of the preferred pattern to achieve this? `I think you should just do this in a separate module, perhaps as a mixin.` - my real world version is in fact within a module, however as a module is a subdivision of a store, any solution in the context of a module should work for a store also. I'm not in favor of a mixin solution as it has static scope and offers no benefit above my solution. Also your linked question is very loosely related to this one, I don't see its relevance. – mikeapr4 Jun 15 '17 at 20:51
  • @mikeapr4 see updated answer for an example. the `state/methods.js` file would be the centralized place to store the logic. To my knowledge, Vuex doesn't offer a way to expose plain methods, so I think you have to use a module outside of Vuex paradigm. As for the linked question, it's just a deeper dive into why getters can't receive input other than state itself. – Matt Jun 15 '17 at 20:59
  • @mikeapr4 Mixed in computed values (and methods) have access to the store in the same way any component does. I'm not sure what you mean by static scope. – Bert Jun 15 '17 at 21:58
  • Fair enough, I follow the pure functional approach, that's an interesting solution. @BertEvans - I follow the mixin approach now also, ignore my static scope point, I was thinking of something else. – mikeapr4 Jun 16 '17 at 02:07
  • If there are no other answers today I'll mark this as accepted. In my case I make several calls to `inRange` from the template with different literal parameters, so creating a computed property for each one (with data/state) wouldn't suit my use case, but it is a valid answer the question here. – mikeapr4 Jun 16 '17 at 12:36
  • 3
    I'm not sure about calling this method anti-pattern. It's even in the official vuex docs: https://vuex.vuejs.org/guide/getters.html#method-style-access – Deleroy Jan 09 '20 at 19:36
  • 1
    @Deleroy as stated, it's just an opinion. I don't think losing the caching makes sense in the majority of cases. YMMV – Matt Jan 10 '20 at 00:31
4

Just to illustrate why I accepted Matt's answer, here is a working snippet, the key point to notice is instead of:

Vuex.mapGetters(['inRange'])

There is a true computed property:

inRange4: function() {
  return this.$store.getters.inRange(4);
}

This, as can be seen from running the snippet, caused the value to be cached correctly. As I stated, this pattern isn't one I can use as I would end up with too many computed properties (inRange1, inRange2, inRange3 etc), however it does answer the question with the example in question.

I have chosen to continue using the code from the question, unchanged.

Note: Matt's answer doesn't match this code exactly, and I believe his intent was that the state from the store would be mapped to the Vue instance, which I see as unnecessary.

const store = new Vuex.Store({
  state: {
    lower: 5,
    higher: 10,
    unrelated: 3
  },
  getters: {
    inRange: state => value => {
      console.log('inRange run')
      return (state.lower <= value) && (state.higher >= value)
    }
  },
  mutations: {
    reduceLower: state => state.lower--,
    incrementUnrelated: state => state.unrelated++
  }
})

new Vue({
  el: '#app',
  template: "<div>{{ inRange4 }}, {{ unrelated }}</div>",
  store,
  computed: Object.assign(
    {
      inRange4: function() {
        return this.$store.getters.inRange(4);
      }
    },
    Vuex.mapState(['unrelated'])
  ),
})

setTimeout(function() {
  console.log('reduceLower')
  store.commit('reduceLower')
  setTimeout(function() {
    console.log('incrementUnrelated')
    store.commit('incrementUnrelated')
  }, 3000);  
}, 3000);
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex/dist/vuex.js"></script>
<div id="app"></div>
mikeapr4
  • 2,830
  • 16
  • 24
1

There seems to be a way around this, create a map with the calculated values and access it as

inrange[4];

I frequently use it to initialize accessors of different kinds, I get an array from my backend and needs to access it by some field (e.g. ID). For the above example it seems reasonable since the range is small:

const store = new Vuex.Store({
  state: {
    lower: 5,
    higher: 10,
    unrelated: 3
  },
  getters: {
    inRange: state => {
      console.log('inRange run')
      var result = {};
      for( var i = state.lower; i < state.higher; i++) {
        result[i] = true;
      }
      return result;
    }
  },
  mutations: {
    reduceLower: state => state.lower--,
    incrementUnrelated: state => state.unrelated++
  }
})

new Vue({
  el: '#app',
  template: "<div>{{ inRange[4] }}, {{ unrelated }}</div>",
  store,
  computed: Object.assign(
    {
      inRange: function() {
        return this.$store.getters.inRange;
      }
    },
    Vuex.mapState(['unrelated'])
  ),
})

setTimeout(function() {
  console.log('reduceLower')
  store.commit('reduceLower')
  setTimeout(function() {
    console.log('incrementUnrelated')
    store.commit('incrementUnrelated')
  }, 3000);  
}, 3000);
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex/dist/vuex.js"></script>
<div id="app"></div>
Samuel Åslund
  • 2,814
  • 2
  • 18
  • 23