ReactヲタがVueに手を出してみる#2

[前回](https://qiita.com/k_tada/items/d41d4ce57cb60a79b35b)からの続き

# 今回やること
前回スルーしてたVuexのactionsに触れていきたい

# APIサーバの準備
今回やりたいことから外れてるので、APIサーバは[json-server](https://www.npmjs.com/package/json-server)で簡単に用意する

まずはインストール

```bash
$ npm i -D json-server
```

んでjsonファイルの作成

```json:db.json
{ "todos": [] }
```

最後にスクリプトを設定して

```diff:package.json
    "scripts": {
        :
        :
+     "server": "json-server db.json --port 3333"
    }
```

`npm run server`で起動

```bash
$ npm run server

> vue-vuex-todo@1.0.0 server /Users/tada/work/workshop/git/vue-vuex-todo
> json-server db.json --port 3333


  \{^_^}/ hi!

  Loading db.json
  Done

  Resources
  http://localhost:3333/todos

  Home
  http://localhost:3333

  Type s + enter at any time to create a snapshot of the database
```

```bash
$ curl localhost:3333/todos
[]
```

OK.

# APIにアクセスするActionを作る
APIコールにはaxiosを使ってます。

```bash
npm i -S axios
```

Action作る過程はバッサリカット。こんな感じ

```js:src/store/todos.js
  // 使わないmutationsは削除済み
  mutations: {
    incrementId (state) {
      state.nextId += 1
    },
    setTodos (state, todos) {
      // TODO ここダサいのでどうにか死体
      state.todos = Array.isArray(todos) && todos.length > 0 ? todos.map(t => ({ ...t, isDone: JSON.parse(t.isDone) })) : []
      state.nextId = Array.isArray(todos) && todos.length > 0 ? Math.max(...todos.map(t => +t.id)) + 1 : 0
      state.isDoneAll = state.todos.every(t => t.isDone)
    }
  },
  actions: {
    async getTodos ({ commit }) {
      try {
        commit('setTodos', (await axios.get('/todos')).data)
      } catch (e) {
        console.err('Failed to get todos', e.message)
      }
    },
    async addTodo ({ commit, dispatch, state }, text) {
      const todo = { id: state.nextId, text, isDone: false }
      commit('incrementId')
      try {
        await axios.post('/todos', todo)
        dispatch('getTodos')
      } catch (e) {
        console.err('Failed to add todo', e.message)
      }
    },
    async toggleTodo ({ commit, dispatch, state }, id) {
      const target = state.todos.find(t => t.id === id)
      if (!target) {
        console.log(`Todo[${id}] is not found`)
        return
      }
      try {
        await axios.put(`/todos/${id}`, { ...target, isDone: !target.isDone })
        dispatch('getTodos')
      } catch (e) {
        console.err('Failed to toggle todo', e.message)
      }
    },
    async toggleAllTodo ({ commit, dispatch, state }) {
      try {
        await Promise.all(
          state.todos.map(async t => axios.put(`/todos/${t.id}`, { ...t, isDone: !state.isDoneAll }))
        )
        dispatch('getTodos')
      } catch (e) {
        console.err('Failed to toggle todo', e.message)
      }
    },
    async deleteTodo ({ commit, dispatch, state }, id) {
      const target = state.todos.find(t => t.id === id)
      if (!target) {
        console.log(`Todo[${id}] is not found`)
        return
      }
      try {
        await axios.delete(`/todos/${id}`)
        dispatch('getTodos')
      } catch (e) {
        console.err('Failed to delete todo', e.message)
      }
    }
  }
```

簡単に説明すると`Action`では基本**非同期処理等を行う**、**Mutationをコミットする**という２つのことを行う。Reduxでいうsagaやthunk等のmiddleware層が行うような処理が該当する。と思われる。

`Action`は引数にコンテキストを受け取るようになっていて、コンテキストには下記のパラメータが含まれる

- commit  
Mutationを発行するためのメソッド
- dispatch  
他のActionを発行するためのメソッド
- state  
現在のステート

# ViewからActionを呼び出す
さっくりとこんな感じ

```diff:src/components/todo/List.vue
  <script>
- import { mapGetters } from 'vuex'
+ import { mapGetters, mapActions } from 'vuex'

  export default {
    name: 'List',
    computed: mapGetters('todos', {
      todos: 'allTodos'
    }),
    methods: {
-     toggleTodo (id) {
-       this.$store.commit('todos/toggleTodo', id)
-     },
-     deleteTodo (id) {
-       this.$store.commit('todos/deleteTodo', id)
-     }
+     ...mapActions({
+       toggleTodo: 'todos/toggleTodo',
+       deleteTodo: 'todos/deleteTodo'
+     })
    }
  }
  </script>
```

Actionを呼び出すにはVuexの`mapActions`を使えばいいっぽい。それだけ。
他のファイルも差分だけ残しとく。

```diff:src/components/todo/Form.vue
  <template>
    <div class="form">
      <div class="toggle-all" @click="toggleAllTodo" />
      <div class="input">
        <input
          type="text"
          class="new"
          placeholder="What needs to be done?"
          :value="text"
          @input="updateText($event)"
-         @keyup.enter="addTodo()"
+         @keyup.enter="addTodoAndClearText()"
        />
      </div>
    </div>
  </template>

  <script>
- import { mapState } from 'vuex'
+ import { mapState, mapActions } from 'vuex'

  export default {
    name: 'Form',
    data () {
      return {
        text: ''
      }
    },
    computed: mapState({
      isDoneAll: state => state.todos.isDoneAll
    }),
    methods: {
      updateText (e) {
        this.text = e.target.value
      },
-     addTodo (e) {
-       this.$store.commit('todos/addTodo', this.text)
+     addTodoAndClearText () {
+       this.addTodo(this.text)
        this.text = ''
      },
-     toggleAllTodo () {
-       this.$store.commit('todos/toggleAllTodo', !this.isDoneAll)
-     }
+     ...mapActions({
+       addTodo: 'todos/addTodo',
+       toggleAllTodo: 'todos/toggleAllTodo'
+     })
    }
  }
  </script>
```

初回読み込み時に一覧を取得する処理を追加して完了

```diff:src/components/TodoApp.vue
  <template>
    <div class="container">
      <Form />
      <List />
    </div>
  </template>

  <script>
+ import { mapActions } from 'vuex'
  import Form from './todo/Form'
  import List from './todo/List'

  export default {
    name: 'TodoApp',
+   mounted () {
+     this.getTodos()
+   },
    components: {
      Form,
      List
-   }
+   },
+   methods: mapActions({
+     getTodos: 'todos/getTodos'
+   })
  }
  </script>
```

これでToDOリストをAPIサーバ側で管理出来るようになりました。

![actions.gif](https://qiita-image-store.s3.amazonaws.com/0/16629/6931de49-8334-d622-f5c7-3df72c68e596.gif)

# まとめ
予想通りというか、APIコールのような非同期処理を行う場合もかなり簡単に実装することができた。~~Sagaなんていらんかったんや。~~
これでVue+Vuexの基礎的な要素は大体網羅できたと思うのでVueの勉強は一旦おしまいかな。