Managing State with Vuex - the Guide I Wish I'd Had

April 02, 2018 0 Comments

Managing State with Vuex - the Guide I Wish I'd Had

 

 


TL;DR: see the vuex flow and/or skip to see the code.

Frontend apps have been growing more and more feature-rich in recent years. "It's for the web" is not a proper reason to deny desktop-like feature requests anymore. At the same time frontends are switching from the traditional MVC model to a more componentized structure, and the need for a solid state management pattern has emerged. After all, the components interacting with each other is a vital part of any bigger app.

Flux is a design pattern released by Facebook, created to structure client-side, component-based applications. There are many implementations of the Flux-pattern but in this post we're going to focus on one: Vuex. This is the guide I wish I'd had when I first started reading about state management with Vuex. There will be code!

Concepts

The key concepts with Vuex are: the state, actions, mutations and getters. The state object contains the application state, and is shared with all the components. Mutations change the state - and they are the only way to change it. Actions commit mutations, the key difference being that mutations can not be asynchronous. We should invoke async actions which commit mutations when the async code has completed. All state mutations must be synchronous! Finally the getters return specific parts of the state for components to use.

The flow in a single image

The flow of data inside store: components create actions which commit mutations, mutations alter the state and getters return parts of it.

You can choose not to use some of the steps described above, but for the sake of completeness I'll go through the flow as it's designed to be used.

The sample app

We're going to take a look at some code, which creates the store for one property, mutates it and returns it for components. The sample app is a concept of an activity calculator of some sorts. The basic idea is that you'll select an exercise you're working with and then add the amount of that exercise done, like stairs climbed, the distance you've ran or the pushups you've done. The app for this example consists of two components: one that selects the exercise and the other one that uses the selected exercise and allows you to mark the "reps" you've accomplished and send the data to a backend service for further processing.

Bring on the code

Let's get going with the code - I did use the vue-cli simple webpack setup to enable ES6 features. First of all, let's create the Vuex store.

The state inside store.js

The state inside the store is just another object, it can contain anything you want.

//store.js 
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex) const state = { selectedExercise: undefined
}
// ...
We have the state object in store.js, it contains the selectedExercise which we'll mutate in a little while.

Actions

Then we're having the action methods, they get the context as their first parameter and the possible payload as the second param. This action creates a mutation by calling context.commit with the name of the mutation and passing possible payload to go with it.

//store.js 
const actions = { selectActiveExercise(context, exercise){ console.log('selecting exercise action, ' + exercise.name) context.commit('selectExercise', exercise); }
}
// ...
Actions commit mutations

Mutations

And then there are the mutations. Mutations get the state as first parameter and an optional payload as second. The action from previous step committed a mutation which calls the selectExercise method which in turn changes the state for real.

//store.js 
const mutations = { selectExercise(state, exercise){ console.log('selecting exercise mutation, ' + exercise.name) state.selectedExercise = exercise }
}
// ...
Mutations altering the state

Getters

The last missing part - the getters exposed by the store. You can call the selectedExercise getter from any of your components and it'll return you that specific portion of the state.

//store.js 
const getters = { selectedExercise(state){ console.log('getting selected exercise') return state.selectedExercise }
}
// ...
Getter returning a part of the state

Exporting the Vuex store

Build up the store and export it so we can use it.

//store.js 
export default new Vuex.Store({ state, actions, mutations, getters
})

Import the store and use it in your app

Initialising the app with the store.

// your app.js/main.js, some code omitted 
import store from './store/store.js' new Vue({ el: '#app', store: store, router: router, template: '<App/>', components: { App }
})
We're importing the store from newly-created store.js and assigning it as a store to our app.

Using the store inside components

Running actions and mutating the state

Now that we've set up the store, we can use it in our components. First of all the exercise-selector component, which triggers the action that selects the active exercise for our context by running the select exercise action which in turn runs the mutation that commits the change to state.

import { mapActions } from 'vuex' 
export default { name: "exercise-selector", methods: { ...mapActions( { selectActiveExercise: 'selectActiveExercise' } ) }
// code omitted...
}
Exercise selector vue component, it maps the selectActiveExercise action for use in our Vue component
<template> <li class="exercise-row" @click="selectActiveExercise" role="button"> <div class="name">{{ exercise.name }}</div> <div class="pointsPerUnit"> <span>{{ exercise.pointsPerUnit }} points per {{ exercise.unit }}</span> </div> </li> 
</template>
Using the mapped "selectActiveExercise" method inside the template

Getters

After taking care of mutating the state, we're mapping the getters defined in the store to our other component. This effectively creates a computed getter method with the name "selectedExercise" for our component.

import { mapGetters } from 'vuex' 
export default { name: "exercise-input", computed: { ...mapGetters([ 'selectedExercise' ]) },
//...
}
Mapping the getter "selectedExercise" from vuex store for use as computed properties

When the getter is in our component's context, we can use it in our template as follows.

<div v-if="selectedExercise"> <h2>Add exercise</h2> <div class="input-container"> <div class="input-selected-name">{{ selectedExercise.name }}</div> <in-put class="input-number" v-on:keyup.enter="addExercise" type="number" placeholder="0" v-model="units"></in-put> <div class="input-unit">{{ selectedExercise.unit }}</div> <div class="input-details">Points {{ selectedExercise.pointsPerUnit}} per {{ selectedExercise.unit }}</div> <button @click="addExercise">Add to your exercises record<span class="forward"></span></button> </div> 
</div>
Mapping the getter "selectedExercise" from vuex store for use as a computed property

So we are using the mapped getter method inside our template. This effectively gets the data from store and is updated automatically when any component commits the mutation that changes the selected exercise.

And that's it, Vuex with a couple of lines of code.

Afterword

I got into Vuex a couple of weeks ago during a get-together with my colleagues. At first all the talk about actions and mutations seemed a bit confusing and complicated, but to see it in a few lines of code makes it quite clear and understandable. And in the end using centralized state does make application development easier as the size of the application gets bigger. When the state changes are reactively rendered in every component, you can focus on the key features which alter the state instead of doing something like emitting events or updating your views manually.

I like it, it beats all the manual scripting and event-based solutions I've seen before. By a mile!


Tag cloud