Vue.jsのコンポーネント間で値の共有をする5つの方法

やりたいこと

Vue.jsでコンポーネント間での値の共有をしたい。
親子関係問わず行えるとOK。

方法

主に以下の5つがありそう。

  • V-bind/Props/Emit
  • State Management Library (etc: Vuex, Redux...
  • EventHub/EventBus
  • BrowserStorage (etc: localStorage, IndexedDB...
  • Server (etc: firebase...

V-bind/Props/Emit

親子関係のコンポーネントだとすごい楽に値を渡せる。

parent.vueのhogeをchild.vueにv-bindで渡し、
child.vueでは、propsで受け取り、
child.vueのemitでparent.vueに値と渡し、 parent.vueのメソッドイベントハンドラhogeを更新。

ディレクト

  • Project
    • parent,vue
    • child.vue

parent.vue

<template>
  <div>
    <child
      :hogehoge="hoge"
      @hogehogeHandler='fromChild'
    >
    </child>
  </div>
</template>

<script>
import child from './child.vue'

export default {
  data () {
    return {
      hoge: 'fuga'
    }
  },
  components: {
    child
  },
  methods: {
    fromChild (msg) {
      this.hoge = msg
    }
  }
}
</script>

child.vue

<template>
  <div>
    <p>{{ hogehoge }}</p>
    <button @click="hogehogeHandler">hogehoge -> fugafuga</button>
  </div>
</template>

<script>
export default {
  props: ['hogehoge'],
  methods: {
    handler () {
      this.$emit('hogehogeHandler', 'fugafuga')
    }
  }
}
</script>

parent.vueの:hogehoge="hoge"のイコールの左側はchild.vueのprops名になり、右側はparent.vueのdataのプロパティ名が入る。

parent.vueの@hogehogeHandler='fromChild'のイコールの左側はchild.vueのemitの第一引数名になり、右側はparent.vueのmethods名または何かしらの処理が入る。

child.vueのthis.$emit('hogehogeHandler', 'fugafuga')は、第一引数にはparent.vueの@hogehogeHandler='fromChild'のイコールの左側が入り、第二引数以降には渡したい値が入る。
今回の場合はfugafugaという文字列をparent.vueの@hogehogeHandler='fromChild'を介しparent.vueのfromChild (msg)の引数で受け取っている。
なお、$emitでたくさん値を渡したい場合は以下のようにobjectにして渡している。

this.$emit('foo', {
  bar: 'yahho',
  baz: 10000,
  qux: {
    quux: 'apikey'
  }
})

State Management Library (etc: Vuex, Redux...

Vueでのコンンポーネント間で値を共有したいならとりあえずVuexを使うことがおすすめ。
筆者が他のライブラリについて詳しくないからなんとも言えないけど、VueでReduxを使うとパッとしないと思ってる(そもそもReduxをよく知らない)。
なのでここでは、Vuexを紹介。

VuexにはStateの管理方法(値の共有の仕方?)には2つの方法が存在する。
- クラシックモード - モジュールモード

今回は、モジュールモードのみ書く。(モジュールモードの方が好きだから)
ビルドは、parcelでします。
スプレッド演算子(...)を利用できるようにbabelやtypescriptでトランスパイルできるように設定しないといけない。

だいぶ複雑なディレクトリになる。
foo.vueとbar.vueで設定した値をindex.vueで見る仕組みを以下に記入。

ディレクト

  • src
    • index.html
    • index.js
    • index.vue
    • components
      • foo.vue
      • bar.vue
    • store
      • index.js
      • modules
        • baz.js

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Vuex</title>
</head>

<body>
  <div id="app"></div>
  <script src="./index.js"></script>
</body>

</html>

index.js

import Vue from 'vue'
import Vuex from 'vuex'
import App from './index.vue'
import { store } from './store'

Vue.use(Vuex)

new Vue({
    el: '#app',
    store,
    render: h => h(App)
})

index.vue

<template>
  <div>
    {{ count }}
    <foo></foo>
    <bar></bar>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import foo from './components/foo.vue'
import bar from './components/bar.vue'

export default {
  name: "App",
  components: {
    foo,
    bar
  },
  computed: {
    ...mapGetters({
      count: 'baz/count'
    })
  }
}
</script>

components/foo.vue

<template>
  <div>
    <button @click="set({ count: 1000 })">set 1000</button>
  </div>
</template>

<script>
import { mapActions } from 'vuex'

export default {
  methods: {
    ...mapActions({
      set: 'baz/set'
    })
  }
}
</script>

components/bar.vue

<template>
  <div>
    <button @click="set({ count: 5 })">set 5</button>
  </div>
</template>

<script>
import { mapActions } from 'vuex'

export default {
  methods: {
    ...mapActions({
      set: 'baz/set'
    })
  }
}
</script>

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import baz from './modules/baz'

Vue.use(Vuex)

export const store = new Vuex.Store({
  modules: {
    baz
  }
})

store/modules/baz.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const state = {
  count: 0
}

const actions = {
  set ({ commit }, { count }) {
    commit("SET", { count })
  }
}

const mutations = {
  SET (state, { count }) {
    state.count = count
  }
}

const getters = {
  count: state => state.count
}

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations
}

流れとしては、index.vueのcount(mapGettesのcount)でstore/modules/baz.jsのstateのcountを参照(購読?)しておく。
components/foo.vueのbutton押された場合(発行?)、
components/foo.vueのbuttonのset(mapActionsのset)で値(1000)を
store/modules/baz.jsのactionsのsetの第二引数で受け取り、
commitでstore/modules/baz.jsのmutationsのSETの第二引数で受け取り、
store/modules/baz.jsのstateに保存する。
保存するとindex.vueのcount(mapGettesのcount)は自動的に1000という値が入る。

index.vueのmapGettersでstore/modules/baz.jsのgettersの準備。
index.vueのmapGettersのcount: 'baz/count'ののkey(count)はdataのプロパティのように利用できる。
例えば、index.vueならthis.countのように利用できるようになっている。
value('baz/count')のスラッシュの左側bazはstore/index.jsのmodules: { baz }で,
スラッシュの右側countはstore/modules/baz.jsのgettersのcount。
ちなみにgetterに引数を持たせることが可能。
他に、mapGettersのstoreのmodule名を省略する書き方もある。

store/modules/baz.jsのcount: state => state.countのkey(count)がindex.vueのcount: 'baz/count'value('baz/count')のスラッシュの右側にあたる。
count: state => state.countvalue(state => state.count)は、store/modules/baz.jsのconst stateのcountを読むようになっている。

Vuexのactionsを利用しなくても構わないが、非同期処理を行いたい場合はactionsのfunctionにasync/awaitを付けて非同期処理を行うことができる
firebaseとかと連携したい場合は利用するかもしれない。

構造は違うけどサンプルリポジトリ

EventHub/EventBus

wip

BrowserStorage (etc: localStorage, IndexedDB...

wip

Server (etc: firebase...

wip