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

今まで「それReactでよくね？」ってVueを避けてたんだけど、そろそろ触ってみようと思います
※ 超初心者向けなのでVue有識者の方々には物足りない記事になってるかと思います

# とりあえず触ってみるもの
Vue + Vuexの構成がReact + Redux的なデファクトスタンダードらしいのでそれを触ってみる
Nuxt.jsは素晴らしいらしいのだけど、それ使っちゃうと多分大事なところ全部Nuxtがやってくれちゃって自分で1から作れなくなるので今回はパス
代わりにvue-cliでwebpackを使ってみようと思ふ

# 作るもの
いつもの通り初回はTODOでも作ってみる

作ったものは[こちら](https://github.com/k-tada/vue-vuex-todo)

# インストール＆環境構築
## vue-cli
```bash
$ npm i -g vue-cli
```

OK.

## Vue環境
```bash
$ vue-cli webpack vue-vuex-todo
? Project name vue-vuex-todo
? Project description A Vue.js project
? Author k-tada <white.rv0925@gmail.com>
? Vue build standalone
? Install vue-router? No
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests Yes
? Pick a test runner jest
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm

   vue-cli · Generated "vue-vuex-todo".


# Installing project dependencies ...
  :
  :
  :
# Project initialization finished!
# ========================

To get started:

  cd vue-vuex-todo
  npm run dev

Documentation can be found at https://vuejs-templates.github.io/webpack
```

OK.

### ちょこっと変更
2018/08/13現在、デフォルトだとbabel-preset-stage-2が入るみたいだけど、stage-3のObject Spread使いたいから面倒くさいからstage-0入れる

```bash
$ npm i -D babel-preset-stage-0
```

んで`.babelrc`書き換える

```diff:.babelrc
  {
    "presets": [
      ["env", {
        "modules": false,
        "targets": {
          "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
        }
      }],
-     "stage-2"
+     "stage-0"
    ],
    "plugins": ["transform-vue-jsx", "transform-runtime"],
    "env": {
      "test": {
-       "presets": ["env", "stage-2"],
+       "presets": ["env", "stage-0"],
        "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"]
      }
    }
  }
```

OK.

## 動確
ここで一度確認

```bash
$ npm run dev
```

でブラウザから[http://localhost:8080/](http://localhost:8080/)にアクセスしたら下記の画面が見えるはず
<img width="632" alt="スクリーンショット 2018-08-13 19.34.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/16629/d06f3f10-5bae-0383-4288-b81cab83f6f0.png">

OK.

# Vue作っていく
Vueだけなら、基本は`src/components`の下に`.vue`ファイルを作っていけばOKっぽい

今回のメインの目的はVueに触ることなので、デザインあまり考えたくなかったから[TodoMVC](http://todomvc.com/)っぽい感じで作ることにした。

VueのコンポーネントはReactと違ってJS in HTMLなので、下記のような感じで作ればいいっぽい。
コンポーネント設計のセンスは生まれつき持ち合わせて無いのでご愛嬌

```vue:src/App.vue
<template>
  <div class="app">
    <div class="container">
      <Logo />
      <TodoApp />
    </div>
  </div>
</template>

<script>
import Logo from './components/Logo'
import TodoApp from './components/TodoApp'

export default {
  name: 'App',
  components: {
    Logo,
    TodoApp
  }
}
</script>

<style scoped>
/* 省略 */
</style>
```

他のコンポーネントもさっくりと

```vue:src/components/Logo.vue
<template>
  <div class="logo">{{text}}</div>
</template>

<script>
export default {
  name: 'Logo',
  data () {
    return {
      text: 'todos'
    }
  }
}
</script>

<style scoped>
/* 省略 */
</style>
```

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

<script>
import Form from './todo/Form'
import List from './todo/List'

export default {
  name: 'TodoApp',
  components: {
    Form,
    List
  }
}
</script>

<style scoped>
/* 省略 */
</style>
```

```vue:src/components/todo/Form.vue
<template>
  <div class="form">
    <div class="toggle-all" />
    <div class="input">
      <input type="text" class="new" placeholder="What needs to be done?" />
    </div>
  </div>
</template>

<script>
export default {
  name: 'Form'
}
</script>

<style scoped>
/* 省略 */
</style>
```

```vue:src/components/todo/List.vue
<template>
  <div class="todos">
    <div class="todo" v-for="todo in todos" :key="todo.id">
      <input
        class="toggle"
        type="checkbox"
        :checked="todo.isDone"
      />
      <div class="text">{{todo.text}}</div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'List',
  data () {
    /* test data */
    return {
      todos: [
        { id: 1, text: 'aaa', isDone: false },
        { id: 2, text: 'bbb', isDone: true },
        { id: 3, text: 'ccc', isDone: false }
      ]
    }
  }
}
</script>

<style scoped>
/* 省略 */
</style>
```

これでこうなる
<img width="694" alt="スクリーンショット 2018-08-13 19.49.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/16629/b1f9c75b-3162-1f0a-acc6-a4732afa1ec9.png">

コミットはこちら
https://github.com/k-tada/vue-vuex-todo/commit/2e0143784cba9cdedc818d1eafadb57bbbaaccd9

# Vuex作ってく
Vuexの詳細な説明は他の記事を参照（なげやり）

## インストール
vue-cli webpackで作った環境にはvuexは入ってないので自前でインストールする

```bash
$ npm i -S vuex
```

## vuex store作る
ReduxではAction,ActionCreator,Reducerと色々用意しないといけないけど、VuexだとStore1つでOK。Ducksパターンっぽい

ReduxのcombineReducerっぽく複数のStoreを扱えるようにしとく。

```js:src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import todos from './todos'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    todos
  }
})
```

続いてToDO管理用のStore

```js:src/store/todos.js
export default {
  namespaced: true,
  state: {
    nextId: 1,
    todos: [],
    isDoneAll: false
  },
  getters: {
    allTodos (state) {
      return state.todos
    },
    activeTodos (state) {
      return state.todos.filter(t => !t.isDone)
    },
    completedTodos (state) {
      return state.todos.filter(t => t.isDone)
    }
  },
  mutations: {
    addTodo (state, todo) {
      state.todos.push({ id: state.nextId, text: todo, isDone: false })
      state.nextId += 1
    },
    toggleTodo (state, id) {
      state.todos = state.todos.map(t => ({ ...t, isDone: t.id === id ? !t.isDone : t.isDone }))
      state.isDoneAll = state.todos.every(t => t.isDone)
    },
    toggleAllTodo (state, isDone) {
      state.todos = state.todos.map(t => ({ ...t, isDone }))
      state.isDoneAll = isDone
    },
    deleteTodo (state, id) {
      state.todos = state.todos.filter(t => t.id !== id)
    }
  },
  actions: {}
}
```

順に説明するとこんな感じ。

- `namespaced: true`  
Storeごとに名前空間をもたせられる。combineReducerされたStateっぽい感じになる。
- `state`  
Storeの持つステート。ReduxのStateと（多分）同じ。
- `getters`  
ステートから値を取り出すためのメソッド。別のStoreのステートも引っ張ってこれるっぽい。Reselect的なアレ。`getters`を経由しなくても直接ステートを参照することも出来るのでステートから何かしら処理したデータをVue側で参照したいときに使う。
- `mutations`  
ActionCreator + Reducer的な。ステートを更新するためのメソッド。基本的にはステートの更新は必ずmutations経由で行う。Viewから直で呼べる。
- `actions`  
非同期処理部分。今は使ってないから空。ToDOをサーバから取得するような処理を書く場合はここに書くのかな。actionsでAPIコールしてmutationsを呼び出してステートを更新するイメージだと思う

# Vueから呼び出す
まずはList

```diff:src/components/todo/List.vue
  <template>
    <div class="todos">
      <div class="todo" v-for="todo in todos" :key="todo.id">
        <input
          class="toggle"
          type="checkbox"
          :checked="todo.isDone"
+         @click="toggleTodo(todo.id)"
        />
        <div class="text">{{todo.text}}</div>
      </div>
    </div>
  </template>

  <script>
+ import { mapGetters } from 'vuex'

  export default {
    name: 'List',
-   data () {
-     /* test data */
-     return {
-       todos: [
-         { id: 1, text: 'aaa', isDone: false },
-         { id: 2, text: 'bbb', isDone: true },
-         { id: 3, text: 'ccc', isDone: false }
-       ]
-     }
-   }
+   computed: mapGetters('todos', {
+     todos: 'allTodos'
+   }),
+   methods: {
+     toggleTodo (id) {
+       this.$store.commit('todos/toggleTodo', id)
+     }
+   }
  }
  </script>
```

ポイントはこの辺。
- `@click`でクリック時のイベントをbindする（`v-on`の省略記法らしい）
- vuexの`mapGetters`で、vueに必要なデータを取り出す
- `this.$store.commit`で、vuexのmutationsを呼び出す

続いてForm

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

  <script>
+ import { mapState } 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)
+       this.text = ''
+     },
+     toggleAllTodo () {
+       this.$store.commit('todos/toggleAllTodo', !this.isDoneAll)
+     }
+   }
  }
  </script>
```

色々差分出てるけど、ポイントはこの辺

- 入力フォームのinputイベントでVueのローカルステートを更新（`updateText`）
- 入力フォームのEnterキー押下イベントでVuexの`addTodo`mutationを呼び出し
- toggleAll系の処理（詳細は割愛）

最後にVueからVuexのStoreを見れるようにmain.jsを修正

```diff:src/main.js
  import Vue from 'vue'
  import App from './App'
+ import store from './store'

  Vue.config.productionTip = false

  /* eslint-disable no-new */
  new Vue({
    el: '#app',
+   store,
    components: { App },
    template: '<App/>'
  })
```

これでOK。こんな感じで動く
![todo.gif](https://qiita-image-store.s3.amazonaws.com/0/16629/59a4f213-e6e2-27c6-9296-84b752713cf7.gif)

# Delete忘れてた（2018/08/14）
```diff:src/components/todo/Form
  <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()"
        />
+       <div class="destroy" @click="deleteTodo(todo.id)">×</div>
      </div>
    </div>
  </template>

  <script>
  import { mapState } 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)
        this.text = ''
      },
      toggleAllTodo () {
        this.$store.commit('todos/toggleAllTodo', !this.isDoneAll)
+     },
+     deleteTodo (id) {
+       this.$store.commit('todos/deleteTodo', id)
      }
    }
  }
  </script>
```

これでOKね.
![deletetodo.gif](https://qiita-image-store.s3.amazonaws.com/0/16629/8d944421-eb4c-a650-a3c0-d4c0a96f7d15.gif)


# まとめ
まだVueの書き方には慣れないけど、ざっと書いてみた感じだとReact + Reduxに比べて楽に書けるように感じた。
おそらく中〜大規模なアプリケーションになると、Redux-sagaのような仕組みが欲しくなると思うけど、軽量アプリケーションならぶっちゃけReact + Reduxを選ぶよりVue + Vuexのほうが楽かもしれない。React + MobXともいい勝負かも。

続くかも。