Learn to build manage state in a Vue.js application using Vuex from the perspective of an Angular dev using NgRx.
Download
In this post I’ll be working with a demo Vue.js application. You can clone the repository or download a zip file of the source code:
Previously
Previously, I wrote about my experience of building a simple to-do list style application using Vue.js from the perspective of an Angular developer. In this post, I’ll continue by using the Vuex library to manage the state of the application.
NgRx is a popular library for managing state in an Angular application. As such, I’ll be writing this post from the perspective of using NgRx. FWIW, NgRx is inspired by the Redux library that is popular for managing state in a React application. I’ve also used Redux and find it to be easy to use and powerful.
Let’s dive in.
Installation
$ npm install vuex --save
$ yarn add vuex
Next, create a new src/store directory and a new src/store/index.js file:
$ mkdir src/store
$ touch src/store/index.js
Open the src/store/index.js file and invoke the Vue.use()
method to use vuex:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
Determine Actions
When working with NgRx, or Redux, or Vuex, we want to start by planning out the actions that will need to be dispatched to the store to mutate the state of our application.
Here are the actions that we will likely need for a to-do application:
- Adding a to-do.
- Clearing or removing all to-do items.
- Completing a to-do item.
- Setting a to-do item to incomplete.
- Removing a to-do item.
Mutations
To get started, let’s define the mutation constants. Create a new src/store/mutation.js file.
Define string constants for each action that will result in a mutation to the state of the application:
export const ADD_TO_DO = 'addTodo';
export const CLEAR_TO_DOS = 'clearTodos';
export const COMPLETE_TO_DO = 'completeTodo';
export const INCOMPLETE_TO_DO = 'incompleteTodo';
export const REMOVE_TO_DO = 'removeTodo';
When using NgRx it is common to define an enum for the “action types” in our application. So, this should look pretty familiar. In fact, before TypeScript 2.4 supported string enums, we often defined the action types when using NgRx by using string constants similar to how we have defined the mutation types in our Vue application.
Now we are ready to define the mutations
object, which we will export so we can wire up our mutations to the Vuex store.
Mutations are the same concept as using reducer()
pure functions with NgRx that would be added to the ActionReducerMap
.
export const mutations = {
[ADD_TO_DO]: function (state, todo) {
todo.id = Math.random();
if (!todo.complete) {
todo.complete = false;
}
state.todos.push(todo);
},
[CLEAR_TO_DOS]: function (state) {
state.todos = [];
},
[COMPLETE_TO_DO]: function (state, todo) {
todo.complete = true;
},
[INCOMPLETE_TO_DO]: function (state, todo) {
todo.complete = false;
},
[REMOVE_TO_DO]: function (state, todo) {
const i = state.todos.map((todo) => todo.id).indexOf(todo.id);
if (i === -1) {
return;
}
state.todos.splice(i, 1);
},
};
Using es2015 object initializer spec for computed properties we declare properties for each action in the mutations
object.
Each handler function receives the state
followed by an optional payload
that is dispatched with the action.
Actions
Next, let’s define the actions. Create a new src/store/actions.js file:
import {
ADD_TO_DO,
CLEAR_TO_DOS,
COMPLETE_TO_DO,
INCOMPLETE_TO_DO,
REMOVE_TO_DO,
} from './mutations';
export default {
addTodo({ commit }, todo) {
commit(ADD_TO_DO, todo);
},
clearTodos({ commit }) {
commit(CLEAR_TO_DOS);
},
completeTodo({ commit }, todo) {
commit(COMPLETE_TO_DO, todo);
},
incompleteTodo({ commit }, todo) {
commit(INCOMPLETE_TO_DO, todo);
},
removeTodo({ commit }, todo) {
commit(REMOVE_TO_DO, todo);
},
};
We define the actions as functions, which accept the context
object along with an optional payload
.
Note that we’re using destructuring assignment in the first argument to each action in order to define a local-scoped variable based on the property in the object.
In our case, we are only concerned with the commit
property.
The commit
instance method enables us to commit a mutation to the store.
The first argument is the type
and the second argument is the action’s payload
.
This is the same concept derived from Redux that is used by NgRx.
Create a new Store()
Open the src/store/index.js file and add the following to create a new Vuex.store()
instance:
import actions from './actions';
import { mutations } from './mutations';
export default new Vuex.Store({
strict: true,
state: {
todos: [],
},
actions,
mutations,
});
We export the default Vuex.store()
instance, specifying the following options:
- The
strict
property is set totrue
. We do this when we are in development so that each object is recursively frozen and cannot be modified outside of a mutator. This is similar to the ngrx-store-freeze module that should be used when in development. Of course, we do not want to run Vuex in strict mode in production. - The initial
state
to have a top-leveltodos
property that is an empty array. - The
actions
property registers the actions that can be dispatched to the store. - The
mutations
property registers the mutations that will mutate (update) the store.
Inject Store
With our actions and mutations defined and the store wired up, our next task is to inject the store
into all of the components in our application.
We’ll do this via the Vue.use()
method that was defined and exported in the src/state/index.js module.
Open src/main.js and specify the store
in the new Vue()
constructor:
import store from './store';
new Vue({
store,
render: (h) => h(App),
}).$mount('#app');
Dispatch Actions
With our store ready to go let’s dispatch()
actions from our App
.
Open src/App.vue and implement the
<script>
import TodoForm from '@/components/TodoForm.vue';
import TodoList from '@/components/TodoList.vue';
import { ADD_TO_DO, COMPLETE_TO_DO, INCOMPLETE_TO_DO } from './mutations';
export default {
name: 'App',
data() {
return {
fixed: false,
todo: {
task: null
}
};
},
components: {
TodoForm,
TodoList
},
computed: {
todos() {
return [
{
task: 'Watch Ozark Season 2',
complete: false
},
{
task: 'Use Vuex in my awesome app',
complete: true
}
];
},
completeTodos() {
return this.todos.filter(todo => todo.complete);
},
incompleteTodos() {
return this.todos.filter(todo => !todo.complete);
}
},
methods: {
onAdd: function() {
this.$store.dispatch(ADD_TO_DO, this.todo);
},
onChange(todo) {
if (todo.complete) {
this.$store.dispatch(COMPLETE_TO_DO, todo);
} else {
this.$store.dispatch(INCOMPLETE_TO_DO, todo);
}
}
}
};
</script>
Note that we use the $store
property within the component that is injected into our component.
Invoke the dispatch()
method to dispatch an action to the store, first specifying the action type
along with an optional payload
.
Devtools
Using the vue-devtools we can inspect the state of our application and observe the actions, and optional payloads associated with the action:
Conclusion
Creating apps using Vue with the Vuex state management pattern + library is easy, robust and very similar to building apps with Angular and NgRx.