Vue Vuex REST TodoMVC example

Small example of TodoMVC implemented using Vue.js and Vuex data store against REST backend.

I could not find a good example of a TodoMVC that uses in-memory data store (like Redux or Vuex) that is synchronized against a REST backend, so I put together this example. It is based on the simplest TodoMVC using Vue.js which does not require any build steps to run. You can find my code in the bahmutov/vue-vuex-todomvc repo.

The problem

We need to add / remove Todo items in the list and store them on the backend. Our front end framework should work against a data store, but the data store should make calls to actually store the data on the backend. Each Todo item will have a title, a completed boolean flag and a random id, generated on the client. Here is a typical todo item:

1
2
3
4
5
{
"title": "learn Italian",
"completed": false,
"id": "6552b6dc4b"
}

There are a LOT of front end solutions to this problem at todomvc.com excepts they all use browser's localStorage to keep the data. I wanted to have a more realistic scenario where a server is actually serving the items.

The web application should do the following 3 things:

  1. On startup, get the list of items from the server
  2. When adding an item, it should first post the item to the server, and only then should add the item to the client's data store
  3. On deleting an item, it should delete the item from the server, and if it has been deleted on the server, then it should delete it on the client side.

In all cases, the server keeps "the true state"; the client-side data store is secondary, and it is updated only if the server has acknowledged the operation.

REST backend

To implement the backend I chose the json-server to create the REST backend with zero code. Just install it using npm i -S json-server command, create a JSON file with the contents shown below and start the server

1
2
3
{
"todos": []
}

1
2
3
4
5
6
7
8
9
10
11
{
"name": "vue-vuex-todomvc",
"version": "1.0.0",
"description": "Simple TodoMVC with Vue and Vuex",
"scripts": {
"db": "json-server --static . --watch data.json"
}
,

"dependencies": {
"json-server": "0.12.1"
}

}

The server will serve the current folder, which allows us to load the index.html page from localhost:3000. It also watches the data.json file, if we change its contents, the server is automatically restarted. We can use this feature to reset items before each test for example.

We can now GET list of items from the localhost:3000/todos endpoints, and we can add new items to it by making a POST request. We can delete a particular item by making DELETE /todos/:id request.

Vuex store

The store is pretty simple, here is its data, and the two getters that we expect to be used from the application.

1
2
3
4
5
6
7
8
9
10
const store = new Vuex.Store({
state: {
todos: [],
newTodo: ''
},
getters: {
newTodo: state => state.newTodo,
todos: state => state.todos
}
})

We can also have additional state there, like "loading" indicator, but for this tutorial I am going to skip these details. See app.js for the entire store.

In addition, the store is going to have mutations and actions. Mutation methods can change the "state" object, and will trigger the Vue DOM updates. Thus these methods are synchronous. For example, here are mutations to set the todos array, add a single todo item, and to clear the "newTodo" text.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const store = new Vuex.Store({
// state
// getters
mutations: {
SET_TODOS (state, todos) {
state.todos = todos
},
ADD_TODO (state, todoObject) {
state.todos.push(todoObject)
},
CLEAR_NEW_TODO (state) {
state.newTodo = ''
}
// other mutations are done the same way
}
})

Actions on the other hand are not mutating the state yet. Actions are called by the outside application code (think UI component code), and can perform asynchronous operations and then call mutations to actually set the new data. Here are three actions to fetch the list of todos from the server and set it on the state, and to clear the "newTodo" string.

I am using axios library to make XHR calls to the backend.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const store = new Vuex.Store({
// state
// getters
// mutations
actions: {
loadTodos ({ commit }) {
axios
.get('/todos')
.then(r => r.data)
.then(todos => {
commit('SET_TODOS', todos)
})
},
addTodo ({ commit, state }) {
if (!state.newTodo) {
// do not add empty todos
return
}
const todo = {
title: state.newTodo,
completed: false,
id: randomId()
}
axios.post('/todos', todo).then(_ => {
commit('ADD_TODO', todo)
})
},
clearNewTodo ({ commit }) {
commit('CLEAR_NEW_TODO')
}
}
})

Not every action is asynchronous, and a single action can trigger multiple mutations. For example, if we were setting "loading" state before making an API call, we could do something like this:

1
2
3
4
5
6
7
8
9
10
loadTodos ({ commit }) {
commit('SET_LOADING', true)
axios
.get('/todos')
.then(r => r.data)
.then(todos => {
commit('SET_TODOS', todos)
commit('SET_LOADING', false)
})
}

Great, how is our store used from the Vue component?

Vue component

First, we tell Vue framework to use Vuex data store.

1
Vue.use(Vuex)

Second, when creating a component, we set the store on it. We trigger actions on the store from the Vue component by dispatching them. And we read the data from the store by connecting computed properties to the store's getters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const app = new Vue({
store,
el: '.todoapp',
// load todos on start
created () {
this.$store.dispatch('loadTodos')
},
// be able to get the data
computed: {
newTodo () {
return this.$store.getters.newTodo
},
todos () {
return this.$store.getters.todos
}
}
})

All commands from the DOM elements should go via component's methods. In this case we have just a few methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const app = new Vue({
store,
el: '.todoapp',
// created
// computed
methods: {
setNewTodo (e) {
this.$store.dispatch('setNewTodo', e.target.value)
},

addTodo (e) {
e.target.value = ''
this.$store.dispatch('addTodo')
this.$store.dispatch('clearNewTodo')
},

removeTodo (todo) {
this.$store.dispatch('removeTodo', todo)
}
}
})

These methods are triggered by the DOM events, defined using Vue template language. For example, method "removeTodo" is called when the user clicks on the "class=destroy" element on each item in the list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<section class="main" v-show="todos.length" v-cloak>
<ul class="todo-list">
<li v-for="todo in todos"
class="todo"
:key="todo.id"
:class="{ completed: todo.completed }">

<div class="view">
<input class="toggle" type="checkbox" v-model="todo.completed">
<label>{{ todo.title }}</label>
<button class="destroy" @click="removeTodo(todo)"></button>
</div>
</li>
</ul>
</section>

And that's it - we have an application, where the DOM events are dispatched to the store; inside the store's actions we can make REST calls to the backend. The store after receiving the response mutates the state. Vue connects everything together - whenever the data in the store changes, the "getters" are going to change, which changes the "computed" properties, triggering the DOM update to show the changed data.

In an ASCII diagram, it would be a cycle of calls triggered from the DOM, going via actions to the backend, then back to the action code, etc.

1
  DOM ----- event -----> component's method ---- store.dispatch ----> store
    ^                                                                 action
    |                                                                    |
    |                                                                    |
component's                                                          REST calls
 computed           Vue => Vuex => Backend => Vuex => Vue              to the
properties                        data flow                           backend
    ^                                                                    |
    |                                                                    |
 getter                                                                  v
    |                                                               back to the
 state.foo = "bar" <---------- mutation <---------- commit -------- action code

Hope it is clear now.