State Helpers for Vuex
Shorter code is great. Even shorter code is even better. In this post, you’ll learn how to drastically shorten the length of your Vuex mutations, making for some really clean code.
#Vue — 20 December 2020
This article has been updated as part of my 2020 blog upgrade. I was going to follow it up with something that doesn’t use Lodash and adds additional helpers, however with Vuex 5 coming into the picture soon, mutations might well be scrapped altogether.
In a Vuex mutation, we normally pass the state and payload through a function that handles the state-change required. But if you’re anything like me, it can be done in a much cleaner way. Yes, there are indeed packages for this, like Pathify, though these can sometimes be a little much, especially when we want to achieve a small sub-set of ‘cleanliness’.
Here’s how it’s normally done:
const mutations = { setName(state, name) { state.name = name }, setPassword(state, password) { state.password = password }}
Sure, that’s neat enough for the job. But what about repetition? We could always use arrow functions for this, which cleans things up a bit:
const mutations = { setName: (state, name) => (state.name = name), setPassword: (state, password) => (state.password = password)}
But perhaps, it’s not as easy to read as we’d like it to be. And, the reptition is still there.
So let’s take another approach, and pass by reference. Almost like a method-access Vuex getter. If not, why not?
const mutations = { name: set('name'), password: set('password')}
Enter the Utility
To achieve this, we’ll need to create a utility/helper that gets us going. In the one I’ve put together, Lodash is being used to do the heavy lifting. The primary reason for choosing it was so that dot notation could be used easily. That said, this article is about the idea, and so you could use whatever tools you have to accomplish the same thing, if you really wanted to.
import _set from 'lodash/set'import _get from 'lodash/get'import _isArray from 'lodash/isArray'import _findIndex from 'lodash/findIndex' /** * @param {String} key * @param {null|any} override */export let set = (key, override = null) => (state, value) => { _set(state, key, override || value)} /** @param {String} key */export let push = (key) => (state, value) => { let piece = _get(state, key) if (_isArray(piece)) { piece.push(value) }} /** @param {String} key */export let remove = (key) => (state, value) => { let piece = _get(state, key) if (_isArray(piece)) { let index = _findIndex(piece, value) piece.splice(index, 1) }}
We could add more methods here, but these are the basic ones that cover a good amount of needs.
Note: I’m prefixing the imports with an underscore for the purposes of preventing naming collisions, and identifying the source of the import (it’s coming from Lodash, so an underscore is appropriate.)
Usage
Right, so how do we use them? As above, we pass the mutating function by reference using the utility/helper we import. Go ahead and save the above to a file called, say, state-helpers.js
.
Then, import the methods you need into your store module:
import { set } from './state-helpers' const state = { name: '', profile: { mobile: '' }} const mutations = { name: set('name'), mobile: set('profile.mobile')} const module = { namespaced: true, state, mutations} export default module
Because we’re using Lodash, we can easily use dot notation to reference a nested piece of state, and still keep the code clean and easy to understand.
Here’s a simple example of how this can be used in a component:
import { mapMutations } from 'vuex' export default { methods: mapMutations('namespace', ['name', 'mobile'])}
Now, the values passed to name
and mobile
will be further passed through the assigned helper (set
) which then performs the mutation when called.
So what if we want to set a mutation that clears something (ie, with a default)? A common example of this would be setting and clearing error messages. My preferences differ here (sometimes), but there’s no harm is using a clearErrors
mutation in an action:
const mutations = { clearErrors: set('errors', {})} const actions = { saveSomething: ({ state, commit }) => new Promise(async (resolve, reject) => { // await an API call; then when it succeeds: commit('clearErrors') resolve() })}
This works because our helper passes an empty object to the mutating method, which checks to see if an override
exists. If it does, use it. If not, use the value passed by reference.
The push
and remove
helpers work in the exact same way as set
, though they don’t support overrides. For pushing to an array, it would be as simple as accounting for the override in the helper-method’s signature and in the called method, as shown in the set
example.