Vue のコンポーネントと Vuex Store を繋げるためのヘルパ vuex-connect を作った
某勉強会中にネタを思いついて、急いで作って LT してその日のうちに npm にアップしたら、翌日Vue 公式に紹介されていてだいぶビビったやつです。Vue と Vuex のヘルパなので、この二つを理解していることが前提になります。
vuex-connect (Github) vuex-connect (npm)
vuex-connect の機能 #
vuex-connect は connect
関数のみ提供しており、やっていることは react-reduxの connect
と同じです。connect
は第一引数に Vuex の getters、第二引数に actions を受け取ります。 また、戻り値として別の関数を返し、こちらの関数には、コンポーネント名と、コンポーネントのコンストラクタを渡します。 最終的に、渡したコンポーネントのプロパティに getter と action をつなげた上位コンポーネント (コンテナ) を返します。
// コンポーネントを定義
const HelloComponent = Vue.extend({
props: {
message: {
type: String,
required: true,
},
updateInput: {
type: Function,
required: true,
},
},
template: `
<div>
<p>{{ message }}</p>
<input type="text" :value="message" @input="updateInput">
</div>
`,
})
// コンポーネントと Store をつなげる
const getters = {
message: (state) => state.message,
}
const actions = {
updateInput: ({ dispatch }, event) =>
dispatch('UPDATE_INPUT', event.target.value),
}
const HelloContainer = connect(getters, actions)('hello', HelloComponent)
なぜ有用なのか? #
これも react-redux で言われていることと同じですが、開発の定石として、フレームワークやライブラリへの依存をなるべく少なくするというものがあります。UI コンポーネントと、状態管理を司るフレームワークを疎結合にするために、Redux では、UI の見た目の実装を行うためのコンポーネントと、Store 内の状態とやり取りするためのコンポーネントを分けるという方針をとっています。これらに関して、前者は Presentational Component、後者は Container Component と呼ばれています (Usage with React | Redux)。
Vue と Vuex で例えると、Presentational Component、Container Component のどちらも Vue のコンポーネントとして実装されます。Presentational Component は Vuex への依存関係を持っておらず、Vue のみで完結しています。Container Component は Vue と Vuex の両方に依存しており、両者をつなげる役割のみを担います。ここで、Vuex を使うのをやめて別の状態管理のフレームワークを使うケースを考えます。Vuex に依存しているコンポーネントは Container Component のみであり、また、その役割は Vue と Vuex をつなげるということのみであるため、Container Component を交換するのみで良くなります。すなわち、Vue と Vuex の依存を最小限にすることで、Presentational Component として実装した Vue コンポーネントが再利用できるようになっているということです。
Vuex の公式ドキュメントを見てみると、上記のようなことは考慮されておらず、Vue と Vuex が密結合になっているように感じたため、vuex-connect を作りました。Vue だと props でバケツリレーするのはちょっと違和感あるのですが、アプリケーションのあちこちで状態を変えられるよりは良いのかなと……。もしくは、action を props に渡すのではなく、events の方に結びつけても良いかもしれないですね。
実装 #
実装したての頃は react-redux とインターフェースを合わせようとして四苦八苦していたこともあり、Vue.prototype._init を上書きしたりしてました。しかし、よくよく考えてみると vuex オプションにそのまま受け取った引数を渡せばいいことに気が付き、かなり簡潔な実装になりました。
https://github.com/ktsn/vuex-connect/commit/f0a254ea5c7b330bcc2446167b970940d67a724b
コンポーネントの props に getter と action を渡したいので、template の部分をがんばって生成しています。
悩み #
connect で生成したコンポーネントに props を渡せるようにするべきかを悩んでいます。react-redux だと渡せるようにしているのですが、正直あんまりそうするケースが思い浮かばないし、データの流れが二股になって複雑になることを懸念しています。また、React の場合は react-router などが props にデータを渡してくるので、それに対処するために props の値を読めるのは有用なのですが、Vue の場合はそういうケースで props を使うことがないので、やるとしても別のアプローチをするべきなのかなーとも思ってます。あと、Vue の props はちゃんとコンポーネント側で定義してあげる必要があるので実装がめんどくさそう……。
最後に #
なんとなく Vuex 使ってる人はものすごく少ないような感じがするのですが、Vuex を使うことがあったら vuex-connect も一緒に使っていただけると嬉しいです。そして contribution もウェルカムです!