Vuex Mapping Getter with Argument - Cached?

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.

Answers:

Answer

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);
    }
  }
});
Answer

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>

Answer

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>

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.