Using Vuex with prop.sync

We’re currently looking at how we handle state updates from custom components, in conjunction with implementing Vuex.

With .sync it automatically creates an @update handler which sets the local state to the received value:

<component name="foo" :value.sync="value"></component>

However, a Vuex store requires changes to be comit()ted.

Does this mean that sync is fundamentally incompatible with Vuex mapGetters(), or is there a workaround?

Right now, we do the following:

<component name="foo" :value="value" @change="onChange"></component>
onChange (name, value) {
    this.$store.commit('path/' + name, value)
}

Are we on the right path?

(Saying that, if we are on the right lines, we could write our own directive to do just this, right?)

After talking about it a bit, we think a helper function would be the way to go:

function mapHandler (module) {
  return function (name, value) {
    this.$store.commit(module, { name, value })
    this.$emit('change', this, this.values)
  }
}

That results in code like the following:

  computed: {
    ...mapGetters({
      values: 'parameters/car'
    })
  },

  methods: {
    onChange: mapHandler('parameters/car')
  }

One of the problems we’re trying to solve here, is to pass the change event on to any parent components.

Is this a fair approach, or are we making work for ourselves, when we could just be deep watching the shared $store object in any parent component?

Yes, I’d say you’re on the right path. One thing I’ve found that can become quite tedious is keeping Vuex in sync with forms. It requires a lot of extra code. One approach I’ve taken is to sync the form with Vuex on the next step or when it’s submitted. If you require it to always be in sync, I’d at least recommend a debounce function coupled with your helper (especially for text inputs).

Hey James,

With this current set of components, we do have a need to update the store immediately.

But we’re doing forms next, so I’ll keep that in mind!

You can use v-model.sync when you write a small helper that creates a computed prop with getter & setter.

This could roughly look like this:

function mapTwoWay(namespace, getter, action) {
  return {
    get () {
      return this.$store.getters[ namespace ? `${namespace}/${getter}` : getter]
    },
    set (value) {
      this.$store.dispatch(namespace ? `${namespace}/${action}` : action, value)
    }
  }
}
5 Likes

That is super-neat!

This sounds really promising for what I’m working on!
Please can you clarify how this would be implemented with a complete example?

Is this along the right lines?

<component name="foo" v-model.sync="value"></component>

along with:

computed: {
    value: mapTwoWay('moduleName', getValue, updateValueAsync)
  },

  methods: {
    mapTwoWay
  }

where mapTwoWay is imported into the component at the top of the <script> tags?

…and then within the component, in a method, I’d use the following to update the v-model prop for example:

this.$emit('update:value', updatedValue)

?

That’S pretty much it, yep. But getValue and duapeValueAsyc would be strings:

mapTwoWay('moduleName', 'getValue', 'updateValueAsync')

Thank you for your solution. It inspired me to do the same thing with vuex-typex. I spent a lot of time to sync props with the vuex store, so I think my solution will be useful for somebody. I know this is old topic, but it’s one of the first links in google about this problem. Here it is:
Store:

    export interface IMapState {
      zoom: number;
    }
    const MapStore = {
      get state(): IMapState { return stateGetter(); },

      setZoom: moduleBuilder.commit(setZoom),

      createSyncedZoomProp: () => {
        return {
          get(): number {
            return MapStore.state.zoom;
          },
          set(newZoom: number) {
            MapStore.setZoom({ newZoom });
          },
        };
      },
    };
    export default MapStore;

and usage (with vue-property-decorator):

    @Component({
      computed: {
        zoom: MapStore.createSyncedZoomProp(),
      },
    })

usage in a template is as a usual prop:
:zoom.sync="zoom"