23
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

React + Reduxを経験してからのstatelessなVue + Vuex構成について

Last updated at Posted at 2018-08-01

筆者について

現フロントエンドエンジニア。
2018年2月当初、配列操作すらおぼつかなかった身から数ヶ月でwebpack覚えてbabel覚えてeslint覚えてVue + Vuex開発のメインプログラミングやってReact + Redux覚えてredux-actions覚えてredux-promise覚えてthunk覚えてrecompose覚えてmaterial-ui覚えてガリガリ開発している。しぬ。

お気持ち

vueでもStatelss Functional Componentライクに書きたい…書きたくない?
=> 今の自分ならこう書くかな、という例。他の案があれば見てみたいなあ。

ここに置いてあります

本題

React + Reduxでの開発経験を生かし、Stateless Functional Componentライクな書き方を採用したVue + Vuex + Routerの環境です。
masterブランチにはサンプルとしてTodoアプリを置いています。

アプリケーションの構成について

厳密ではありませんが、Atomic Designを採用しています。
components/pagesに対してcontainerコンポーネントを1対1で作成し、vuexと紐付け、子コンポーネントへpropsをリレーする構成としました。
vuexを採用しているのはrouterでの遷移時にデータを保持するためです。

src  
  * index.js
  * -components   
      - atoms
      - molecules
      - organisms
      - templates
      - pages
  * -containers  
  * -routes
  * -store

Container層

責務を分割しロジック部分のみを担保します。思想自体はReact + ReduxのContainerと同一です。

container

<template>
  <Content 
    :todos="todos"
    :temp="temp"
    :handleSubmit="addTodo"
    :handleKeyUp="updateTemp"
    :alreadyExists="alreadyExists"
  />
</template>

<script>
import { mapState, mapActions, mapMutations, mapGetters } from 'vuex';
import Content from '~/components/pages/Todo.vue';

export default {
  computed: {
    ...mapState({
      todos: state => state.todoStore.todos,
      temp: state => state.todoStore.temp
    }),
    ...mapGetters(['alreadyExists'])
  },
  methods: {
    ...mapActions(['addTodo']),
    ...mapMutations(['updateTemp'])
  },
  components: {
    Content
  }
};
</script>

Components層

責務を分割しviewの部分のみを担保します。
Stateless Functional Componentを強く意識し、各コンポーネントにdata(reactで言うstate)は持たせません。
Container層から渡ったpropsを元にviewのみに注力します。

components
<template>
  <div>
    <h2>TodoApp</h2>
    <TodoForm 
      :handleKeyUp="handleKeyUp"
      :handleSubmit="handleSubmit"
    />
    
    <Error v-if="alreadyExists" />

    <TodoList 
      :todos="todos"
    />
  </div>
</template>

<script>
import Error from '~/components/atoms/Error.vue';
import TodoForm from '~/components/organisms/TodoForm.vue';
import TodoList from '~/components/organisms/TodoList.vue';

export default {
  props: {
    handleSubmit: Function,
    handleKeyUp: Function,
    todos: Array,
    alreadyExists: Boolean
  },
  components: {
    Error,
    TodoForm,
    TodoList
  }
};
</script>

storeについて

combineReducerのイメージでvuexのmoduleを活用します。
好みですが、reduxではactionCreatorでガッチリとpayloadを作成、reducerは愚直にpayloadをstateに反映するのみとしたかったため、vuexにおいてもmutationsはstate反映のみ、actionsでpayload作成と言う形をとりました。

todoStore
let id = 0;

export default {
  state: {
    temp: '',
    todos: []
  },
  mutations: {
    updateTodos: (state, payload) => (state.todos = payload),
    updateTemp: (state, payload) => (state.temp = payload)
  },
  actions: {
    addTodo: ({ commit, state, getters }, callback) => {
      const newItem = {
        name: state.temp,
        done: false,
        id: id + 1
      };
      const newState = state.todos.concat(newItem);

      if (!getters.alreadyExists) {
        commit('updateTodos', newState);
        commit('updateTemp', '');
      }
      callback && callback();
    }
  },
  getters: {
    alreadyExists: state => {
      const { todos, temp } = { ...state };
      return todos.some(todo => todo.name === temp);
    }
  }
};

store
import Vue from 'vue';
import Vuex from 'vuex';
import todoStore from '~/store/todoStore';

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    todoStore
  }
});

export default store;

採用パッケージについて

今回はvue-cliを使わず、よく使うreact用テンプレートに近づけました。eslint + prettier連携や.vue内のlintも採用しています。
styled-components採用型React + Reduxに比べloaderの多い構成になりました。

package.json
"devDependencies": {
  "babel-cli": "^6.26.0",
  "babel-core": "^6.26.3",
  "babel-eslint": "^8.2.6",
  "babel-loader": "^7.1.5",
  "babel-plugin-root-import": "^6.1.0",
  "babel-plugin-transform-object-rest-spread": "^6.26.0",
  "babel-preset-env": "^1.7.0",
  "css-loader": "^1.0.0",
  "eslint": "^5.2.0",
  "eslint-config-prettier": "^2.9.0",
  "eslint-config-vue": "^2.0.2",
  "eslint-import-resolver-babel-plugin-root-import": "^1.1.1",
  "eslint-plugin-import": "^2.13.0",
  "eslint-plugin-node": "^7.0.1",
  "eslint-plugin-prettier": "^2.6.2",
  "eslint-plugin-promise": "^3.8.0",
  "eslint-plugin-standard": "^3.1.0",
  "eslint-plugin-vue": "^5.0.0-beta.1",
  "html-loader": "^0.5.5",
  "node-sass": "^4.9.2",
  "prettier": "^1.14.0",
  "sass-loader": "^7.0.3",
  "style-loader": "^0.21.0",
  "vue-html-loader": "^1.2.4",
  "vue-loader": "^15.2.6",
  "vue-style-loader": "^4.1.1",
  "vue-template-compiler": "^2.5.16",
  "webpack": "^4.16.3",
  "webpack-cli": "^3.1.0",
  "webpack-dev-server": "^3.1.5"
},
"dependencies": {
  "babel-polyfill": "^6.26.0",
  "vue": "^2.5.16",
  "vue-router": "^3.0.1",
  "vuex": "^3.0.1"
}

キリがないのでここまで!
ありがとうございます。

23
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?