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

やりたいこと

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

方法

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

  • V-bind/Props/Emit
  • State Management Library (etc: Vuex, Redux...
  • EventHub/EventBus
  • WIP: BrowserStorage (etc: localStorage, IndexedDB...
  • WIP: 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を使うは、Vue.jsの良さみたいなものと相性が悪い気がしてならない。
なのでここでは、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.countのvalue(state => state.count)は、store/modules/baz.jsのconst stateのcountを読むようになっている。

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

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

EventHub/EventBus

ディレクトリ

src/
├── Events.js
└── components
    ├── Child.vue
    └── Parent.vue

components/Parent.vue

<template>
  <button @click="add">Parent</button>
</template>

<script>
import { eventHub, EVENT_ADD } from '../Events';

export default {
  name: 'Parent',
  methods: {
    add() {
      eventHub.$emit(EVENT_ADD, "new item");
    }
  }
}
</script>

components/Child.vue

<template>
  <ul>
    <li v-for="(item, index) in items" :key="index">
      {{ item }}
    </li>
  </ul>
</template>

<script>
import { eventHub, EVENT_ADD } from '../Events';

export default {
  name: 'Child',
  data() {
    return {
      items: []
    }
  },
  mounted() {
    eventHub.$on(EVENT_ADD, this.addItem);
  },
  beforeDestroy() {
    eventHub.$off(EVENT_ADD, this.addItem);
  },
  methods: {
    addItem(item) {
      this.items.push(item);
    }
  }
}
</script>

Events

import Vue from 'vue';

export const eventHub = new Vue();

export const EVENT_ADD = 'add';

流れ

  1. Events.js で Vue インスタンスを生成。ついでのイベント名を設定。
  2. イベントを受信したいコンポーネント (ここでは Child.vue) に、イベント用のVueのインスタンスとイベント名をimport。
  3. $on メソッドでイベントを受信できるように設定。このときに、$on メソッドの第1引数には、イベント名を、第2引数には、イベント時に発生する 関数 を設定する。
  4. beforeDestroy()時に、$off メソッドで受信できるようにしたイベントを削除するようにしておく。
  5. イベントを発生させたいコンポーネント (ここでは Parent.vue) に、イベント用のVueのインスタンスとイベント名をimport。
  6. $emit メソッドでイベントを発生できるように設定。$emit メソッドの第1引数には、イベント名を、第2引数には、イベント時に発生する 関数に与える値 を設定する。

簡単にまとめると、
1. $on でイベントをlisten
2. $emit でイベントを発生
3. $off でイベントlistenを終了

参考

WIP: BrowserStorage (etc: localStorage, IndexedDB...

ディレクトリ

src/
├── Storage.js
└── components
    ├── Child.vue
    └── Parent.vue

components/Parent.vue

<template>
  <button @click="add">Parent</button>
</template>

<script>
import { addItem } from '../Storage';

export default {
  name: 'Parent',
  methods: {
    add() {
      addItem('new item');
    }
  }
}
</script>

components/Child.vue

<template>
  <ul>
    <li v-for="(item, index) in items" :key="index">
      {{ item }}
    </li>
  </ul>
</template>

<script>
import { getItems } from '../Storage';

export default {
  name: 'Child',
  data() {
    return {
      items: []
    }
  },
  mounted() {
    this.items = getItems();
  }
}
</script>

Storage.js

const KEY_ADD = 'add';

export function getItems() {
  const raw = localStorage.getItem(KEY_ADD);
  return JSON.parse(raw) || [];
}

export function addItem(item) {
  const items = getItems();
  items.push(item)
  return localStorage.setItem(KEY_ADD, JSON.stringify(items));
}

流れ

Server (etc: firebase...

wip