Efficiently understanding and using Nuxt + Vuex
Nuxt.js is a great framework. I have really enjoyed its convention over configuration approach. In the JS world this is incredibly refreshing to see and a huge time saver while developing. With that leaves a bit of a gap in documentation and real-world usage from being so new. Without further delay, let’s dive into understanding how Nuxt and Vuex work well together.
Why do I need this in my app?
One of the tenants of functional programming is writing small functions that do one thing really well. We often go astray from this with tight deadlines, or a lot of pressure to fix a hot issue quickly. But at some point that comes back to haunt us, and creates an opportunity to revisit designs that no longer fill their purpose. Maybe more than once in our careers we find a variable being mutated as a side effect of another event or function call as the source of a bug?
Vuex provides a one-way highway for handling application states. A laypersons way to think about this is to say “managed global variables that don’t let you mutate them directly”. By the end of this I hope you will be armed with all the knowledge to scream “wrong!” to that statement.
To clear this up more, lets look at the architecture:
Vuex is everything inside the dotted green box. You’ll see below we’ll have a state, a mutation, and some actions in our Vuex store. When you make a new file in Nuxt’s store
folder, that file will likely have some or all of these things included inside it. When a component is rendering, it has access to Vuex state and in turn can be used for whatever that component needs to function. Once rendered, it can dispatch or mutate the store, therefore causing anything else using that store to update.
Modules vs Single Store
Honestly the official documentation is very confusing about which way one should approach configuring Vuex. It is my opinion if you have more that three API calls or state management items to handle the modular approach is required. Outside of quickly getting something working at the beginning of a Nuxt project the single-store approach becomes just as painful as using global variables.
Knowing that modules are the best way to go, the documentation suggests building an index.js
file in the store
directory. Unless you plan on configuring Vuex with some fancy plugins or changing its configuration, index.js
should never exist in the store
folder. Instead each ‘widget’ needs its own file.
State
State is a simple function that returns the object stored inside it. Typically I build something like store/cars.js
export const state = () => ({
list: [],
car: {}
})
This gives me a place to store my collection of cars, and a place to store the ‘current’ car.
In your component html you can then access it using $state.cars.list
, and in your javascript via this.$state.cars.list
. That is all fine if it is read-only. If you wanted to use that in a form, or use v-model then it won’t work. How annoying right? But that actually makes a lot of sense if you look at that picture above again. If you could change state directly, then you skip the parts that will let the rest of your app know it has changed. To get around using a deeply dot-notation path you want to use the mapState
helper.
computed: {
...mapState({
cars: state => state.cars.list,
car: state => state.cars.car
})
}
It’s very important to keep your state as flat as possible. Deeply nested objects in a state lose reactivity. For example, if your state contains something like store.state.cars.car.color
, mutations on color would need to be forced to reload. It is better to make a new store module just for a single car
then. I tend to start out using the state.things.thing
pattern and then refactor to state.thing
when I see I have a need to do many operations on the thing. Then you state would contain the attributes for thing
, restoring attributes to their reactive state. This is because if Vuex had to walk down a deep chain of object keys there is a huge performance hit, as well as a possibility of a never-ending loop.
Getters
Think of getters as computed properties for a store. They should read the current state of the module and return something. It should give some meaning to what it is expected to present. Meaning full names are very important!
totalCars: state => {
return state.cars.length
},
blueCars: state => {
return state.cars.filter(car => car.color === "blue")
}
Getters are always read-only values in your views. They can be used in combination with computed properties however, and you could provide a custom setter:
computed: {
blueCars: {
get() { return this.$getters['cars/blueCars'] },
set(car) { this.$store.dispatch('cars/paintBlue', car)
}
}
While there is a mapGetters helper, I’ve never had a huge need for it. Most of the time the components will do this work. That’s not saying don’t use it, just that its more likely to take a value from the store and alter it in a component. However if multiple components share reading these values and need to read updates, getters are a great choice.
Mutations
This is how to commit values in state. You don’t call it directly because a mutation is a reactive event and we would want our app to know when something changed.
export const mutations = {
set(state, cars) {
state.list = cars
},
add(state, value) {
merge(state.list, value)
},
remove(state, {car}) {
state.list.splice(state.list.indexOf(car), 1)
},
setCar(state, car) { state.car = car }
}
With this I have access to call $store.commit(‘cars/set’, [{id: 1, model: "Tacoma", brand: "Toyota"}])
, etc. This is useful in components with the fetch
, asyncData
, and nuxtServerInit
functions when you want to prefill the store with default values, or to use it as a set
handler in a computed property.
In components you can use the mapMutations helper to quickly get mutator function calls mapped to your component:
methods: {
...mapMutations({
setCars: 'cars/set'
})
}
Which means in your component you can access it via this.setCars([{id: 1, model: "Tacoma", brand: "Toyota"}])
.
I mentioned above about moving between state.things.thing
to state.thing
modules. In that case you would want to set up a mutation function that can easily map the object keys into the state. To do you would need something like:
setCar(state, form) {
let keys = Object.keys(form);
keys.forEach((key) => state[key] = form[key])
},
Of course you could validate your state here and prevent unexpected keys from polluting state as well. It all depends on app requirements.
I want to point out that mutating deeply nested values is also error prone and likely to cause Vuex to reject the mutation. Trying to mutate $store.state.cars.car.tire.brand
would tell you it is not allowed. In that case, it would be best to create a car.js
module that handles a single car, and therefore its state contains the attributes for a single car entity. Try to keep your state object as flat as possible. Create new stores when deep nesting mutations are used.
Actions
These are useful for any asynchronous activity the application needs. Especially useful for calling a back end server, or when non-blocking mutations are needed. Let me give an example for both scenarios.
export const actions = {
async get({commit}) {
await this.$axios.get('cars')
.then((res) => {
if (res.status === 200) {
commit('set', res.data)
}
})
},
async show({commit}, params) {
await this.$axios.get(`cars/${params.car_id}`)
.then((res) => {
if (res.status === 200) {
commit('setCar', res.data)
}
})
},
async set({commit}, car) {
await commit('set', car)
}
}
Here we defined three actions, get
, show
, and set
. Get would fetch our entire car collection, and on success set the store with the list the server gives back. Show is the same, but we can pass in query parameters. This is useful for setting up searches, or any other scenario you can imagine where params are needed. Last is an addition onto our previous mutation, an action calling the mutation ‘set’ from above. Maybe we have a list of 15,000 cars and that is causing lag in the UI when committed? This way we can set up a successful future for using promises or async/await calls.
Plugins
Highly recommend reading the top 5 plugins used for Vuex. Simply create an store/index.js
and import them there as recommended by their respective configurations.
Strict Mode
Nuxt by default configures Vuex to use strict mode in development. It can be disabled in store/index.js
if you want, but doing so is a bad idea. Turning it off allows for mutation calls to come from anywhere, effectively breaking the rules of the architecture. In production it is disabled, but you’ve already written the app knowing the rules aren’t broken, so it becomes a performance boost when vuex no longer worries about checking for this problem in production.
A common error that happens is trying to mutate state using v-model=”$state.cars.car.color”
which won’t work because this is effectively the read-only version of the value. Instead it should use either a computed property, or a combination of the mapState/mapMutations helpers.
Review
It took me a bit of unlearning some bad habits from my past Javascript code to really appreciate the value that Vuex brings to an application. I don’t think it is a required thing for every app; that would be narrow minded to believe that. There are many great implementations of state management in the JS world, but Nuxt has embraced Vuex and for now it works great.
Having a source of truth for state in a user interface saves a lot of time in development. Backing it with reactivity to changes, a consistent API, and great ecosystem of plugins gives us a toolbox to solve problems quickly.
In Nuxt
Nuxt provides a fetch
method in pages. This is very useful for hooking into actions prior to page rendering. It’s common in pages to see a chunk of code like:
async fetch({store}) {
await store.dispatch("cars/get")
}
Which would call the ‘get’ action in the cars module, calling the back end and setting the cars state to the API response.
Vuex Helpers
Vuex has mapping helpers that make it easy to DRY the calls to the store. Instead of using $store.state...
everywhere it is cleaner to use mapState
. There are helpers for actions, mutations, and getters also.
computed: {
...mapState({
cars: state => state.cars.list
})
}
This creates a function call that lets us reference this.cars
instead of this.$store.state.cars.list
. Pretty clear which of those is much easier to read.
The mapGetters
is using the exact same arguments as mapState
, with the notable exception that it will not allow mutations. Getters are read-only and typically useful for formatting data.
What mapState is doing looks like this:
computed: {
cars: {
get() {
return this.$store.state.cars.list
},
set(val) {
this.$store.commit('cars/set', val)
}
}
}
This is also a useful pattern for when you need to perform custom mutations or any other pre-commit logic required. Remember though these should be very short functions so try to keep the logic in get/set minimal.
You can also use mapMutations
to pull mutation functions into templates:
methods: {
...mapMutations('cars', ['set', 'anotherMutation'])
}
Then call this.set(val)
instead of this.$store.commit('cars/set', val)
Conclusion
I hope this clears up a lot of the nuances and confusion about Vuex. It is such a great part of coding with Vue that knowing how to get the most out of it is a boost to productivity.
Edit: There is a follow up if you want more software patterns.
Reference:
Heads up: I wrote a follow up that talks about some useful snippets and design patterns for growing apps.