[PLUGIN] Vuex Pathify - makes working with Vuex easy


#1

I’ve just released a new plugin for Vuex, called Pathify:

Pathify makes working with Vuex easy, with a declarative, state-based path syntax:

Pathify’s core helpers provide one-liner read, write or sync to any store property or sub-property:

Get or set data without syntax juggling or worrying about implementation:

Set up one or two-way data binding on any store value without bloat or fuss:

Wire multiple properties (or sub-properties) using array, object and wildcard formats:

Map store actions in exactly the same way, even using wildcards:

Set up your store – no matter how complex – in a single line:

The end result is:

  • a reduction in structure and syntactic complexity by 2x - 3x
  • a reduction in Vuex-related code of up to 14x
  • significantly less cognitive overhead
  • zero store boilerplate
  • one-liner wiring
  • cleaner code

You can check the docs here:

Live demos are here:


#2

I am really thankful for what you have done!
I was searching for the right store to use - the obvious choice is of course Vuex but it has quite a lot boilerplate (not as much as redux but for me it is enough to start thinking about something else) especially when used in components.
When I saw your solution to the boilerplate problem I recognized that this is the right thing to do - using a stable and full featured store as Vuex with an abstraction which makes the work with it enjoyable


#3

using a stable and full featured store as Vuex with an abstraction which makes the work with it enjoyable

You pretty much summed it up!

Thanks, and enjoy / tweet about it :rofl:


#4

I wanted to check if I understood the correct way Pathify works.
Sorry for the long post, but currently because of my perhaps bad understanding of how people use vuex vs how I use it, I fail to see the benefit of Pathify, but I’d love to use it!


I have used vuex about 1 year.
I’m having a hard time understanding Pathify’s rationale, because for me I have never built any boilerplate and have been fine without… Perhaps there’s a lot of things I’m currently doing wrong, but I’m unaware of it. I never experienced any downsides with my way of using vuex though.

Below I have 3 questions about how I use vuex, and if it’s bad I’d love to know why. Then I have 2 questions about Pathify.

1. Set state from actions, not mutations.

In the beginning I would often experience having to rewrite mutations because I suddenly needed to access a getter in the mutation or dispatch another action. But when I realised I might as well do this inside the action:
state.someProp = thisNewThing
I kind of just didn’t see any benefits of using mutations in the first place. I never experienced trouble with setting the state straight from actions. To me it feels like you just limit yourself by using mutations as you’re not able to use getters and dispatch.

2. Why do people set mutations for each mutation?

A few mutations I do use are things like:

updateState (state, payload) {
  Object.keys(payload).forEach(key => {
    Vue.set(state, key, payload[key])
  })
}

This way I can use updateState from all over my app and just throw an object with just the properties I want to update, and they’ll be overwritten onto the state. Why would I waste time setting up a separate mutation for each single value?

3. Why do people use mapGetters, mapActions, mapState, etc.

Whenever I create a new vue-component as a template I simply add these 4 lines:

  computed: {
    get () { return this.$store.getters },
    state () { return this.$store.state },
  },
  methods: {
    commit (action, payload) { return this.$store.commit(action, payload) },
    dispatch (action, payload) { return this.$store.dispatch(action, payload) },
  }

Then straight from the template I can reference things like so:

<div v-for="item in get.list">{{ item.name }}</div>
<!-- or -->
<button @click="dispatch('addItem')">Add</button>

I hardly write any extra logic in methods or computed-properties inside my actual vue-components, as this would prevent it from ever being re-used.
To be honest, I think the syntax of the map thing looks weird, and I never saw any benefit. Is my method worse for any reason?

4. Now for the Pathify questions:

(A) The benefit I see of using Pathify in my case is to be able to do mutations from actions without having to write the mutation logic, is this correct?
eg.

doThisAction ({state, dispatch},
thisNewThing) {
  // instead of:
  state.someProp = thisNewThing
  // use:
  window.store.set('someProp', thisNewThing)
}

Is there a better way to point to store.set from inside an action? Eg. Could I add set to the destructing of the store values in the first parameter of the action function?
doThisAction ({state, dispatch, set},

(B)
Most actions I have update the state and then dispatch('patch').
Is the following true?
Let’s take the following state:
state: { settingsOpened: false }
my current action would be:

openSettings ({state, dispatch}, value) {
  state.settingsOpened = value
  dispatch('patch', {fields: ['settingsOpened']})
}

if I rename my action to setSettingsOpened() then inside a vue-component I could use this.$store.set('SettingsOpened', true) and because of Pathify it would automatically use my action if it exists.
If there is no action called setSettingsOpened it will look for a mutation called SET_SETTINGSOPENED, but if this mutation also does not exist, will it just set the state directly if the state value settingsOpened exists?

I’d love to really understand the benefits of using Pathify, as currently I am not seeing them and this makes me think that I’m misinterpreting the way I should use vuex in the first place.


#5

Yo Luca,

Thanks for the questions, let me answer them for you.

It’s quite a lot of questions, so let me break them out into a couple of replies; one for Vuex one for Pathify; to make it easier to continue or contribute to separate threads :slight_smile:


#6

Vuex questions:

Hopefully I’ll cover the main points, but perhaps others will chime in if I miss something important.

Questions 1 - 3, you’re essentially saying “I don’t like how Vuex works, or how much work Vuex takes, but I’ve found these hacks kind of work so I’m using them”.

Whilst this might work when you work on your own, it won’t fly in a team environment. You’ll be the guy people complain about on Stack Overflow because you decided to cut corners :stuck_out_tongue:

The main issue with most of your suggestions is explicitness.

To answer your questions specifically:

1. Set state from actions, not mutations.

The docs imply that this does work but you’re doing it wrong.

State should only be set from mutations, by design (though this is changing in the next version of Vuex I understand).

If you use Vue DevTools, the commits make it trackable and provide time travel debugging:

They also trigger Vuex plugins that subscribe to mutations; things like Vuex Persisted State.

2. Why do people set mutations for each mutation?

The main reason is an explicit API. Other developers will:

  • be able to look at your store and know what is writeable
  • look at your component code and see explicit tasks
  • look at DevTools and see an explicit history

Other reasons:

  • not all commits are 1:1 updates. Compound commits (reset(), increment(), login()) might do more than set one property. Having the actual JS code in these commits is more manageable than a blanket “updateState()” function
  • Related to the above, the store should (mainly) be responsible for deciding how its state is changed. By passing everything to an updateState() function and passing an object, you’re essentially asking / trusting the outside code to do the work

You could level the argument at Pathify that make,mutations() makes things less explicit, and you’d be right in a way, but the documentation makes it clear that mutations are a 1:1 pairing with state, or you could be explicit if you care that much:

const mutations = make.mutations(state, [
  'foo',
  'bar',
  'baz',
])

3. Why do people use mapGetters, mapActions, mapState, etc.

I’m not such a fan of the Vuex helpers either, hence writing Pathify.

Your approach is pretty weird though; including all getters and the entire state tree in each component!

The main issue (apart from just being yuk!) is that your component API should be explicit, wiring and exposing just the properties they need; this makes the design of each component clear for other developers (and future you!) as well as testable and useful in Vue DevTools.

Another way to think of it would be “what if I needed to change my store tomorrow?” (probably won’t happen) but you can see how having clear wiring in the code rather than templates makes your components more resilient.

You won’t have much fun when you start using modules either:

<div v-for="item in get['foobar/list']">{{ item.name }}</div>

Using mapState() and mapGetters() allows you to pull in just the properties you need:

computed: {
  ...mapState('foobar', [
    'list',
    'x',
    'y'
  ])
}
<div v-for="item in list">{{ item.name }}</div>

If you want to see some real-world comparisons between Pathify, Vuex helpers, and manual JavaScript, check out the Code Comparison section of the Pathify demo site:

Click the links on the side to see the actual component and store code used for each.


#7

Pathify questions:

Question 1

The benefit I see of using Pathify in my case is to be able to do mutations from actions without having to write the mutation logic, is this correct?

Hopefully by this point you will have answered this question in your head!

So, no.

The store still needs the mutations, and you still need to call them.

I would need to know more about your store stucture to advise you properly, but as discussed, each store / module should be explicit about its data structure and external I/O.

But essentially, Pathify allows you write LESS code (which is clearly what you want) but still using Vuex in the correct way. For your example:

// store
import { make } from 'vuex-pathify'

// clearly-defined state
const state = {
  someProp: 0
}

// makes mutations
const mutations = make.mutations(state)

// debug
console.log(mutations) // { SET_SOME_PROP: function () { ... } }

export default {
  state,
  mutations
}
// component
import { sync } from 'vuex-pathify'

export default {
  computed: {
    someProp: sync('someProp')
  }
}

Regarding your desire to write less code:

  • Refer to everything by the state name only; no setSomeProp() or SET_SOME_PROP - Pathify handles all the naming conversion, for setup and wiring
  • No writing of actions or mutations
  • No horrible hacks like pulling in the entire state
  • No global setting of window.store

Regarding the code you posted with this question, it’s clear you don’t quite understand Vuex mutations.

That could could have been written:

actions: {
  doThisAction ({commit}, thisNewThing) {
    commit('SET_SOME_PROP', thisNewThing) // Use CONST_CASE or camelCase, but it must match your commit naming
  }
}

Note that commit MUST have a corresponding mutation handler!

As you put in your previous questions, you could use a “catch-all” handler but your begin to break down the explicit nature of your store. See my answer to Question 2 in the previous post.

So back to Pathify, this is where make.mutations() comes in! It automatically creates the correctly-named mutation function based on your state and naming scheme.

Question 2

Most actions I have update the state and then dispatch(‘patch’). Is the following true? …

Yep, you pretty much got it here!

So, back-tracking slightly, Pathify is designed to make it easy to update your store’s state in a 1:1 manner. That is, get value / set value.

As such, it uses the state property names as the single naming method:

store.get('settingsOpened')
store.set('settingsOpened', true)
computed: {
  settingsOpended: sync('settingsOpened')
}

Unlike Vuex where there are multiple ways to address a given value (depending on your naming scheme):

store.state.settingsOpened
store.getters.getSettingsOpened
store.dispatch('setSettingsOpened', true)
store.commit('SET_SETTINGS_OPENED', true)

But what Pathify does internally is to reference or call the correct Vuex members anyway by converting intention and path to the right thing:

See the Pathify 101 to see just how it does this.

So in answer to your question:

  1. You still need a mutation function (make.mutations() will do this for you)
  2. Consider not using an action at all, Pathify will derive the mutation name automatically
  3. Consider using sync() to 2-way wire your component rather than dispatching actions

So your final, much-simplified code would be:

Store

const state = {
  settingsOpen: false
}

const mutations = make.mutations(state)

export { state, mutations }

Component

<button @click="isOpen = !isOpen">{{ isOpen ? 'Hide' : 'Show' }} Settings</button>
<div v-show="isOpen"> ... </div>
// component
export default {
  computed: {
    isOpen: sync('settingsOpen')
  }
}

Hope that helps.

And yes, you are fundamentally abusing Vuex though misunderstanding it!

I’d advise getting to know Vuex a little better first, before putting all your faith in Pathify :stuck_out_tongue:


#8

Vuex Pathify has just had its 1.1 release:

Added

  • Ability to create new sub-properties on the fly
  • call() helper to map actions using the same syntax as get() and sync()
  • registerModule() helper to register wildcard members for dynamic modules
  • State can now be passed to make.* as a function

Changed

  • Wildcards can now appear anywhere in the last segment of a path
  • Wildcards targeting module properties must now be explicit, i.e. foo/* rather than foo*
  • deep option format is now 0: disabled, 1: read-write, 2: read-write-create
  • deep option can now be changed at any time
  • Payload#update() now returns the updated state, enabling manual addition of new reactive properties
  • Unknown modules now throw error rather than log to console (for Nuxt compatibility)
  • sync() now reads only from state and not getters
  • Removed only parameter from make.* helpers; function now takes single state / keys parameter

Fixed

  • Invalid computed property paths now return empty functions
  • Fixed bug in sync where invalid paths would cause error message to error
  • Fixed invalid wildcard bug which caused Nuxt to bomb
  • Fixed setting of sub-properties when using mutations and actions
  • Vuex getter functions now return as functions not values for computed properties
  • Fixed bug with module-level wildcard get() not returning getters
  • Fixed error message displaying original package name

Removed

  • Removed component helpers set() function

#9

Nice to see this improving further :slight_smile:

But the listed changes seem to make this a major version if you were to follow semver, don’t they?


#10

Some of the changes could also be described as fixes or defensive programming (sync reading only from state), better error handling (preventing errors, or in the case of Nuxt throwing better errors) or no practical effect (deep options equate to previous booleans, Payload#update still works on existing properties; the assignment is when you need to manually add new properties)

The removal, I realised, could never be used anyway due to how computed properties work.

The only questionable one might be wildcards, but I think the docs all illustrated the explicit usage anyway.

So I think a minor version will fly, but you raise a valid point.

You think that’s a fair assessment or am I abusing semver / potentially disrespecting my users?

EDIT: Actually, you’re right removing the only property from make is a breaking change. I guess I should patch and depreciate the old functionality…


#11

I like what you have done here alot…
I will be recommending this to my team…


#12

@shaydoc thanks!

In my current role we’re using Vuex / Flux less and less as it happens.

This certainly makes it easier when you need to use it though.


#13

I am interested. Why are you using Vuex less and less?
What have you decided to use as an alternative?


#14

It depends, on a bunch of things.

The prevailing narrative is “put it in Vuex, better safe than sorry!”. We’ve found it just adds needless thick, impermeable layers where none are needed.

If you look at what you need to store and where, much of it just doesn’t need to be shared.

Additionally, we’ve increasingly come to see passing API calls through Vuex as an anti-pattern. Usually, a component needs some API data, so just get it. Why jump through the hoops of store setup, state mapping and action dispatching?

As such, I recently wrote Axios Actions which is an alternate layer to Vuex Actions:

It bundles endpoints as callable, reusable services, with a bunch of API-specific functionality like load state and errors. It makes it really easy to call endpoints, and you can include it in your component’s data and it becomes reactive.

If you like you can still commit its results to Vuex, or just let the ApiEndpoint or ApiResource classes manage the data for you, which is much better for CRUD (slightly over-simplified example follows):

<template>
  <ul v-if="endpoint.items && endpoint.items.length">
    <li v-for="item in endpoint.items">{{ item }}</li>
  </ul>
</template>

<script>
import axios from '../lib/axios'
import ApiEndpoint from 'axios-actions'

const endpoint = new ApiEndpoint(axios, 'comments/:id')

export default {
  data () {
    return {
      endpoint: endpoint.when('create update delete', () => this.endpoint.index())
    }
  },
  
  mounted () {
    this.endpoint.index()
  }
}
</script>

When it comes to big, complex components, we’ve found that creating ES6 classes with all their own methods to update themselves are much more flexible and expressive than writing Vuex stores, and you skip all the setup of both the store and the mapped state / getters / mutations / actions.

Rather than the coupling being with an impermeable store, set by path, it’s with a lightweight, flexible class which you just pass in:

class Thing {
  things: Array<Thing>
  
  constructor (data) {
    Object.assign(this, data)
  }

  addThing (thing) {
    this.things.push(new Thing(thing))
  }

  bar () {
    // do something with thing
  }
}

You instantiate the class, pass via a prop and have the component interact with it directly:

<thing :thing="thing" />
<template>
  <ul>
    <li v-for="item in thing">{{ item.blah }}
      <ul if="item.things">
        <li v-for="thing in item.things">{{ thing }}</li>
      </ul>
    </li>
  </ul>
</template>

<script>
export default {
  props: {
    thing: Thing
  },

  methods: {
    onAddThing (thing) {
      this.thing.addThing(thing)
    }
  }
}
</script>

Because Vue makes anything inside a component reactive anyway, all the logic can be inside the instance, or its child instances, and our components can stay light. You can pass around instances inside or outside components, in tests, in the console, you name it.

I’m glad I know Vuex, but there’s really only a few things we need it for.

Since taking it out of a bunch of places in the app, we’re much more productive, and it’s been much easier to model the data for the data it is, not the store we need it to be.


#15

I see the logic in what you have said. I am not a massive Vuex/Flux fan everything. The uses cases for sharing state limited on my application also, and I tend to agree about api calls through Vuex as an anti-pattern.

This kind of composition interests me

  <data-comp :url="endpoint">
       <my-comp slot-scope={data, isloading, error, add, edit, delete}  
                @add="onAdd" 
                @edit="onEdit" 
               @delete="onDelete" />  
  </data-comp>

#16

I don’t know if I would go that far, but I have seen people create a vuex store for every crud endpoint, which I don’t like.

I’m personally guilty of using too many vuex store with no reason. Right now I try to avoid vuex until it becomes necessary.


#17

Yeah, I saw that in the Adam Wathan videos. It’s very cool!

It would work really well with Axios Actions; I see a demo coming on


#18

That sounds promising.
I am looking at options for building a widget plugin system for an editor integration currently and using this web component style approach is very appealing.


#19

I agree that I wouldn’t call it an anti-pattern. I think it’s a per use case scenario. Not all api calls have to go through the store, but if it makes sense then there’s no reason to avoid it.

I also agree that the app doesn’t have to have a 1:1 component to Vuex state at all times. Sometimes the state only relates to that one component (a form for example) in which case ensuring a 1:1 state with Vuex is just tedious and can be unnecessary.


#20

Its interesting. It always comes down to decision making and identifying what is right for Vuex and what is not. It is a useful library in the correct context.