Brian Love
Google Developer Expert in Angular, software engineer and skier located in Denver, CO

Vuex for NgRx Devs

Reading time ~5 minutes

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 to true. 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-level todos 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:

Incomplete To Dos

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.

Brian Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Denver and I ski (a lot).