Last updated at
info
この記事は最終更新から1年以上経っています。情報が古くなっている可能性があります。
この記事はVue.js+Vuexなアプリケーションを、React+Reduxでよく用いられるContainer Componentパターンで設計する手法や、そのメリット/デメリット等について記載したものです。
一般的に、Vue.jsである程度の大きさのアプリケーションを作る際は、Vuexを使って状態管理をすることが多いと思います。その際の書き方や設計として、Vue.js公式では以下のようなものを推奨しており、一般的にもこれが採用されているようです。
<!-- todo-list.vue -->
<script>
import { mapActions, mapState } from 'vuex'
export default {
// StateをComponentで扱えるように流し込んでいる
computed: mapState({
todos: state => state.todos
}),
// ActionsをComponentから呼べるように流し込んでいる
methods: {
...mapActions(['completeTodo'])
}
}
</script>
<template>
<ul class="todo-list">
<li
v-for="todo in todos"
:key="todo"
class="todo"
@click="() => completeTodo(todo)"
>
{{todo}}
</li>
</ul>
</template>
<style scoped>
.todo-list {/* スタイル */}
.todo {/* スタイル */}
</style>
ぱっと見た感じ良さそうに見えます。 しかし、この設計は以下のような理由から、非常に良くない設計になっていると言えるでしょう。
これらの問題を解決し、良い設計に変える方法は簡単です。
スタイルやデザインに関心のあるUIのコンポーネント
と、Storeについて関心のあるロジックのコンポーネント
に分ければ良いのです。
このUIのコンポーネント
とロジックのコンポーネント
を分離し、それらを実際のアプリケーションで上手く実装できるようにしたものが、"Presentational/Container Component Pattern"です。
Presentational/Container Component Patternはコンポーネント設計パターンの一つです。 この設計手法ではコンポーネントを"Presentational Component"と"Container Component"の2つに分類してコンポーネントツリーを組み立てていきます。
Presentational Componentは「どのように見えるか」について関心を持つコンポーネントです。 基本的にマークアップやスタイルをゴリゴリと書いていき、このコンポーネントを使う側に対して抽象化を提供します(Props等)。
Container Componentは「何を表示するか/どのように変更を行うか」についての関心を持ちます。
このコンポーネントは基本的にPresentational ComponentのPropsに対してStateやActionsを流し込みます。
マークアップやスタイルは原則ここには書きません。ラップ用の<div>
くらいは書いてもいいということになっていますが、書かないに越したことはないです。
先ほどのサンプルコードをこの手法で書くと以下のようになります。
<!-- components/todo-list.vue -->
<script>
export default {
props: {
todos: { type: Array, default: () => [] }
},
methods: {
complete(todo) {
this.$emit('complete', todo)
}
}
}
</script>
<template>
<ul class="todo-list">
<li
v-for="todo in todos"
:key="todo"
class="todo"
@click="() => complete(todo)"
>
{{todo}}
</li>
</ul>
</template>
<style scoped>/* 同じ */</style>
<!-- containers/todo-list.vue -->
<script>
import { mapActions, mapState } from 'vuex'
import TodoList from 'src/components/todo-list'
export default {
components: { TodoList },
computed: mapState({
todos: state => state.todos
}),
methods: ...mapActions(['completeTodo'])
}
</script>
<template>
<todo-list :todos="todos" @complete="todo => completeTodo(todo)"/>
</template>
記述量が増えてしまいましたが、components/todo-list.vue
からはStoreの依存が消え、Props(とevent)によるインターフェイスのみになり、暗黙的な状態をなくすことができました。
この設計手法について調べていると、おそらく殆どの記事が「React+Reduxのためのコンポーネント設計」のような書き方をしていると思いますが、そんなことはありません。
実際にReact周辺でもReact+ReduxだけではなくReact+RelayやReact+Apollo、React+MobXでも使われることのある手法です。
また、View層に関してもReactである必要はありません。上記のサンプルコードがそのいい例ですが、Alt-React系のライブラリやもちろんVue.jsでも導入することができます。
この設計手法が最も使われているReact+Reduxではreact-reduxでは、connect
というHOC(コンポーネントをラップするコンポーネント)によってContainer Componentを簡単に作成することができます。
そして素晴らしいことに、vuex-connectというライブラリのおかげでVue.js+Vuexでも簡単にStoreにつながったContainer Componentを作成することができます。
vuex-connectを使うと、先ほどのcontainers/todo-list
を以下のように書くことができます。(jsファイルです)
// containers/todo-list.js
import { connect } from 'vuex-connect'
import TodoList from 'src/components/todo-list'
export default connect({
stateToProps: {
todos: state => state.todos
},
methodsToEvents: {
complete: ({ dispatch }, todo) => dispatch('completeTodo', todo)
}
})('todo-list', TodoList)
StateとActionsを対象コンポーネントのPropsにマッピングする、ということが明示的な非常にシンプルな書き方ができます。
サンプルコードだけじゃわかりづらいと思うので実際にvuex-connectを使ってContainer Component Patternを使ったシンプルなTODOアプリケーションを作成しました。
pocka/vue-container-component-example
src/components
配下にサブディレクトリが切られているのはAtomic Designによる区分けなのであまり気にしないでください。
メリット
デメリット
設計的にはメリットしかなく、パフォーマンス上のデメリットも無いに等しいので(個人の感想です)、プロジェクトで導入できそうであれば是非使ってみることをお勧めします。
この記事の説明でよくわからなかった場合はリンク先の解説を読むとわかりやすいと思います。